En esta página
Interfaces y type aliases
Interfaces y type aliases
TypeScript tiene dos mecanismos principales para dar nombre a la estructura de los objetos: las interfaces y los type aliases. Aunque se solapan en muchos casos, cada uno tiene características únicas que los hacen más adecuados para diferentes situaciones. Entender sus diferencias y sus puntos fuertes te permitirá diseñar código más expresivo y mantenible.
Type aliases: nombrar cualquier tipo
Un type alias simplemente da un nombre a un tipo existente. Puede nombrar cualquier cosa: primitivos, uniones, intersecciones, funciones, tuplas, objetos o tipos utilitarios.
// Alias para tipos primitivos
type ID = string;
type Porcentaje = number;
type Activo = boolean;
// Alias para un objeto
type Punto = {
x: number;
y: number;
};
// Alias para una función
type Comparador<T> = (a: T, b: T) => number;
// Alias para una unión
type StringONúmero = string | number;
// Alias para una tupla
type Coordenadas = [latitud: number, longitud: number];Los type aliases no crean tipos nuevos; crean nombres alternativos para tipos existentes. Cuando TypeScript reporta un error, puede usar el nombre del alias o desempaquetarlo según el contexto.
Interfaces: contratos para objetos y clases
Las interfaces están diseñadas específicamente para describir la forma de los objetos. Tienen una sintaxis más orientada a objetos y algunas características que los type aliases no tienen.
interface Punto {
x: number;
y: number;
}
interface Punto3D extends Punto {
z: number;
}
const punto: Punto3D = { x: 1, y: 2, z: 3 };La diferencia sintáctica más visible es que las interfaces usan extends para heredar, mientras que los type aliases usan intersección (&).
Propiedades opcionales y de solo lectura
Tanto interfaces como type aliases soportan propiedades opcionales (?) y de solo lectura (readonly):
interface ConfigurarServidor {
host: string;
puerto: number;
ssl?: boolean; // Opcional: puede no estar presente
timeout?: number; // Opcional con valor por defecto en la función que lo usa
readonly versión: string; // No se puede modificar después de la creación
}
function iniciarServidor(config: ConfigurarServidor): void {
const ssl = config.ssl ?? false; // Valor por defecto para opcionales
const timeout = config.timeout ?? 30000;
console.log(`Iniciando en ${config.host}:${config.puerto} (SSL: ${ssl})`);
}
// config.versión = '2.0'; // ❌ Error: Cannot assign to 'versión' because it is a read-only propertyDiferencia clave entre `?` y `| undefined`
interface ConOpcional {
valor?: string; // La propiedad puede no existir en el objeto
}
interface ConUnión {
valor: string | undefined; // La propiedad debe existir, pero puede ser undefined
}
const obj1: ConOpcional = {}; // ✅
const obj2: ConUnión = {}; // ❌ Error: property 'valor' is missing
const obj3: ConUnión = { valor: undefined }; // ✅Con exactOptionalPropertyTypes: true en tsconfig, esta distinción se vuelve aún más estricta.
Extendiendo interfaces
Las interfaces se pueden extender de una o múltiples otras interfaces:
interface EntidadBase {
readonly id: string;
readonly creadoEn: Date;
actualizadoEn?: Date;
}
interface Persona extends EntidadBase {
nombre: string;
email: string;
}
interface Empleado extends Persona {
departamento: string;
salario: number;
fechaIngreso: Date;
}
interface Gerente extends Empleado {
equipoIds: string[];
presupuesto: number;
}Cada nivel de extensión hereda todas las propiedades de los padres. Gerente tiene todas las propiedades de EntidadBase, Persona, Empleado, y las propias.
Intersección de types: combinación horizontal
Con type aliases, la combinación se hace con el operador &:
type ConTimestamps = {
creadoEn: Date;
actualizadoEn?: Date;
};
type ConAuditoria = {
creadoPor: string;
actualizadoPor?: string;
};
type EntidadAuditada = ConTimestamps & ConAuditoria & {
readonly id: string;
};
const entidad: EntidadAuditada = {
id: 'ent-001',
creadoEn: new Date(),
creadoPor: 'usuario-123',
};La intersección combina todos los tipos en uno. Si dos tipos tienen una propiedad con el mismo nombre pero tipos incompatibles, la intersección resulta en never para esa propiedad (un tipo imposible de satisfacer).
Index signatures: objetos con claves dinámicas
Cuando no conoces las claves de un objeto de antemano pero sí conoces el tipo de los valores:
interface GlosarioTerminos {
[término: string]: string; // Clave: cualquier string, Valor: string
}
const glosario: GlosarioTerminos = {
TypeScript: 'Superconjunto tipado de JavaScript',
Interface: 'Contrato que describe la forma de un objeto',
Generic: 'Componente reutilizable parametrizado por tipo',
};
// Cuidado: con noUncheckedIndexedAccess, el tipo es string | undefined
const definición = glosario['TypeScript']; // string | undefinedLimitación de los index signatures: si tienes tanto un index signature como propiedades específicas, las propiedades específicas deben ser compatibles con el tipo del index:
interface Mixto {
[clave: string]: string | number; // Index signature
nombre: string; // ✅ string es subconjunto de string | number
edad: number; // ✅ number es subconjunto de string | number
// activo: boolean; // ❌ boolean no es subconjunto de string | number
}Una alternativa más precisa es Record<string, T> o Map<string, T>:
// Record: tipo utilitario para objetos con claves tipadas
type PreciosPorMoneda = Record<string, number>;
type EstadoPorRol = Record<'admin' | 'editor' | 'lector', boolean>;Declaration merging: la característica exclusiva de interfaces
Las interfaces con el mismo nombre en el mismo scope se fusionan automáticamente. Esto no es posible con type aliases.
interface Config {
host: string;
puerto: number;
}
interface Config {
ssl: boolean;
// Ahora Config tiene host, puerto y ssl
}
const config: Config = { host: 'localhost', puerto: 3000, ssl: true }; // ✅Esto es especialmente útil para module augmentation: extender tipos de librerías externas sin modificar sus archivos.
// Extender la interfaz Request de Express:
declare module 'express' {
interface Request {
usuario?: UsuarioAutenticado;
}
}
// Ahora en cualquier handler de Express:
app.get('/perfil', (req, res) => {
const usuario = req.usuario; // TypeScript conoce este campo
});Patrones prácticos con interfaces
Tipado de respuestas de API
interface ErrorAPI {
código: string;
mensaje: string;
detalles?: string[];
}
interface RespuestaExitosa<T> {
datos: T;
paginación?: {
página: number;
porPágina: number;
total: number;
totalPáginas: number;
};
}
type Respuesta<T> = RespuestaExitosa<T> | { error: ErrorAPI };
async function obtenerUsuario(id: string): Promise<Respuesta<Usuario>> {
const res = await fetch(`/api/usuarios/${id}`);
if (!res.ok) {
const error: ErrorAPI = await res.json() as ErrorAPI;
return { error };
}
const datos: Usuario = await res.json() as Usuario;
return { datos };
}Interfaces para configuración de componentes
interface PropiedadesModal {
readonly abierto: boolean;
titulo: string;
contenido: string;
onCerrar: () => void;
onConfirmar?: () => void;
variante?: 'info' | 'advertencia' | 'peligro' | 'éxito';
tamaño?: 'pequeño' | 'mediano' | 'grande';
}Separar lectura de escritura
Un patrón muy útil es tener interfaces separadas para crear y leer datos:
// Lo que necesitas para crear un usuario
interface CrearUsuarioDto {
nombre: string;
email: string;
contraseña: string;
rol?: 'admin' | 'editor' | 'lector';
}
// Lo que retorna la API (sin contraseña, con metadatos)
interface UsuarioDto {
readonly id: string;
nombre: string;
email: string;
rol: 'admin' | 'editor' | 'lector';
readonly creadoEn: string; // ISO string desde la API
avatarUrl?: string;
}
// Lo que el cliente actualiza
interface ActualizarUsuarioDto {
nombre?: string;
avatarUrl?: string;
}Tipos utilitarios con interfaces y types
TypeScript incluye tipos utilitarios que transforman interfaces existentes:
interface Usuario {
id: string;
nombre: string;
email: string;
edad: number;
activo: boolean;
}
type UsuarioParcial = Partial<Usuario>; // Todas las props opcionales
type UsuarioCompleto = Required<Usuario>; // Todas las props requeridas
type UsuarioInmutable = Readonly<Usuario>; // Todas las props readonly
type UsuarioPreview = Pick<Usuario, 'id' | 'nombre' | 'email'>; // Solo algunas
type UsuarioSinId = Omit<Usuario, 'id'>; // Todas menos algunasEstos tipos utilitarios son especialmente poderosos con genéricos, que exploraremos más adelante.
interface vs type: la decisión final
La comunidad TypeScript ha convergido en estas convenciones:
Usa interface para:
- Contratos de objetos y clases (
interface Usuario,interface Repositorio). - Cuando necesites extender con
extendsde forma jerárquica. - Cuando necesites declaration merging (module augmentation).
- APIs públicas de librerías (los errores son más claros).
Usa type para:
- Uniones y tipos condicionales (
type Estado = 'activo' | 'inactivo'). - Tipos calculados y transformados (
type Parcial = Partial<Config>). - Funciones y callbacks (
type Handler = (evento: Event) => void). - Tuplas y tipos primitivos renombrados.
En la práctica, muchos equipos usan interface por defecto y type cuando necesitan algo que interface no puede hacer. Lo más importante es la consistencia dentro de un proyecto.
En la próxima lección profundizamos en los tipos de unión e intersección, incluyendo el poderoso patrón de discriminated unions para modelar estados complejos.
Inicia sesión para guardar tu progreso