Tema
Interfaces y types
Casi todo en una app son objetos: usuarios, productos, pedidos, respuestas de APIs. En esta lección aprenderás a describir la forma de esos objetos con interface y type, a marcar propiedades opcionales o de solo lectura, a combinar tipos y a tipar los datos que llegan de una API.
Concepto
Una interface o un type es como el plano de una casa: no es la casa, pero dice qué cuartos debe tener y de qué tamaño. Cualquier objeto que cumpla el plano es válido.
ts
// Con interface
interface Usuario {
id: number;
nombre: string;
email: string;
}
// Con type (alias de tipo)
type Producto = {
id: number;
nombre: string;
precio: number;
};
const ana: Usuario = { id: 1, nombre: 'Ana', email: 'ana@correo.com' };
const mouse: Producto = { id: 10, nombre: 'Mouse', precio: '349' };
// ❌ Type 'string' is not assignable to type 'number'.TypeScript usa tipado estructural: no importa cómo se llame el tipo, sino que la forma coincida. Si un objeto tiene todas las propiedades requeridas con los tipos correctos, es compatible.
Propiedades opcionales y readonly
ts
interface Cliente {
readonly id: number; // no se puede modificar después de crearse
nombre: string;
telefono?: string; // opcional: puede no existir
empresa?: string;
}
const c: Cliente = { id: 7, nombre: 'Carlos' }; // ✅ sin teléfono ni empresa
c.nombre = 'Carlos Ruiz'; // ✅
c.id = 8;
// ❌ Cannot assign to 'id' because it is a read-only property.
// Una propiedad opcional es tipo | undefined: compruébala antes de usarla
console.log(c.telefono?.replace(/\s/g, '') ?? 'Sin teléfono');readonly es superficial
readonly impide reasignar la propiedad, pero si su valor es un array u objeto, su contenido sí se puede cambiar. Para arrays usa readonly string[].
Ejemplo completo
Un modelo realista de una tienda en línea:
ts
// Tipos reutilizables
type Moneda = 'MXN' | 'USD';
type EstadoPedido = 'pendiente' | 'pagado' | 'enviado' | 'entregado';
interface Direccion {
calle: string;
ciudad: string;
cp: string;
referencias?: string;
}
interface LineaPedido {
productoId: number;
nombre: string;
cantidad: number;
precioUnitario: number;
}
interface Pedido {
readonly id: string;
cliente: { nombre: string; email: string }; // objeto en línea
direccionEnvio: Direccion; // usa otro tipo
lineas: LineaPedido[]; // array de objetos
moneda: Moneda;
estado: EstadoPedido;
creadoEn: Date;
notas?: string;
}
// Función que trabaja con el tipo
function totalPedido(pedido: Pedido): number {
return pedido.lineas.reduce(
(suma, l) => suma + l.cantidad * l.precioUnitario,
0
);
}
const pedido: Pedido = {
id: 'PED-1024',
cliente: { nombre: 'María', email: 'maria@correo.com' },
direccionEnvio: { calle: 'Av. Juárez 120', ciudad: 'Puebla', cp: '72000' },
lineas: [
{ productoId: 1, nombre: 'Café 1kg', cantidad: 2, precioUnitario: 320 },
{ productoId: 5, nombre: 'Taza', cantidad: 1, precioUnitario: 150 }
],
moneda: 'MXN',
estado: 'pagado',
creadoEn: new Date()
};
console.log(totalPedido(pedido)); // 790Métodos en interfaces
ts
interface Carrito {
items: LineaPedido[];
agregar(item: LineaPedido): void; // sintaxis de método
total: () => number; // sintaxis de propiedad-función
}Ambas formas funcionan; lo verás a fondo en Funciones tipadas.
Extender: extends
Las interfaces pueden heredar de otras con extends. Evita repetir campos:
ts
interface Entidad {
readonly id: number;
creadoEn: Date;
actualizadoEn: Date;
}
interface Usuario extends Entidad {
nombre: string;
email: string;
}
interface Administrador extends Usuario {
permisos: string[];
}
const admin: Administrador = {
id: 1,
creadoEn: new Date(),
actualizadoEn: new Date(),
nombre: 'Ana',
email: 'ana@empresa.mx',
permisos: ['usuarios:leer', 'usuarios:editar']
};
// Puedes extender de varias a la vez
interface ConAvatar { avatarUrl: string; }
interface PerfilPublico extends Usuario, ConAvatar {}Intersecciones con type
Con type, el equivalente a extender es la intersección (&): "tiene todo lo de A y todo lo de B".
ts
type Entidad = { id: number; creadoEn: Date };
type DatosProducto = { nombre: string; precio: number };
type Producto = Entidad & DatosProducto;
const p: Producto = {
id: 1,
creadoEn: new Date(),
nombre: 'Lámpara',
precio: 590
};
// Muy útil para agregar campos "al vuelo"
type ProductoConStock = Producto & { stock: number };interface vs type: ¿cuál uso?
Para objetos, ambos sirven y son casi intercambiables. Las diferencias:
Solo type puede:
- Definir uniones:
type Id = string | number. - Definir literales, tuplas y alias de primitivos:
type Rango = [number, number]. - Usar tipos mapeados y condicionales (lo verás en Utility types).
Solo interface puede:
- Fusionarse (declaration merging): si declaras dos veces la misma interface, TS junta sus campos. Útil para extender tipos de librerías (por ejemplo, agregar
usuarioalRequestde Express). - Usar
extends, que da mensajes de error un poco más claros y es algo más eficiente para el compilador en jerarquías grandes.
ts
// Fusión de interfaces
interface Ventana { titulo: string; }
interface Ventana { ancho: number; }
// Ventana ahora tiene titulo y ancho
// Esto con type daría error: Duplicate identifierBuena práctica
Elige una convención y sé consistente. Una regla muy común: interface para objetos (modelos, props) y type para todo lo demás (uniones, literales, tuplas, tipos derivados). Si tu equipo usa solo type, también está bien.
Index signatures
A veces no conoces los nombres de las claves, pero sí el tipo de los valores. Por ejemplo, un diccionario de traducciones o un inventario:
ts
interface Inventario {
[sku: string]: number; // cualquier clave string → valor number
}
const stock: Inventario = {
'CAF-001': 25,
'TAZ-010': 3
};
stock['LAM-500'] = 12; // ✅
stock['TAZ-010'] = 'pocos';
// ❌ Type 'string' is not assignable to type 'number'.
// Puedes combinar con propiedades fijas (deben ser compatibles)
interface Traducciones {
idioma: string;
[clave: string]: string;
}Error común
Con un index signature, TS asume que cualquier clave existe: stock['NO-EXISTE'] tiene tipo number, aunque en realidad sea undefined. Activa noUncheckedIndexedAccess (ver Instalación y tsconfig) para que el tipo sea number | undefined y te obligue a comprobar.
Record
Record<Claves, Valor> es un atajo para crear objetos tipo diccionario. Brilla cuando las claves son una unión de literales, porque TS exige que estén todas:
ts
// Equivale a { [clave: string]: number }
const visitas: Record<string, number> = { inicio: 120, contacto: 15 };
// Con claves conocidas: TS exige las tres
type Estado = 'pendiente' | 'enviado' | 'entregado';
const etiquetas: Record<Estado, string> = {
pendiente: 'Pendiente de pago',
enviado: 'En camino',
entregado: 'Entregado'
};
// Si agregas 'cancelado' a Estado, TS marcará este objeto
// hasta que agregues su etiqueta. ¡Imposible olvidarlo!
const colores: Record<Estado, { fondo: string; texto: string }> = {
pendiente: { fondo: '#fef3c7', texto: '#92400e' },
enviado: { fondo: '#dbeafe', texto: '#1e40af' },
entregado: { fondo: '#dcfce7', texto: '#166534' }
};Caso real: tipar objetos de una API
Cuando consumes una API, describe la respuesta tal como llega y, si hace falta, crea tu propio tipo "limpio" para la app:
ts
// 1. Lo que devuelve la API (en inglés, con nulls)
interface ApiUser {
id: number;
name: string;
email: string;
address: {
city: string;
zipcode: string;
};
company: { name: string } | null;
}
// 2. Lo que usa tu app
interface Usuario {
id: number;
nombre: string;
email: string;
ciudad: string;
empresa: string;
}
// 3. Función que transforma de uno a otro
function normalizarUsuario(u: ApiUser): Usuario {
return {
id: u.id,
nombre: u.name,
email: u.email.toLowerCase(),
ciudad: u.address.city,
empresa: u.company?.name ?? 'Independiente'
};
}
// 4. Uso con fetch
async function obtenerUsuarios(): Promise<Usuario[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const datos: ApiUser[] = await res.json(); // confiamos en la API
return datos.map(normalizarUsuario);
}Separar el tipo de la API del tipo de tu app hace que, si la API cambia, solo tengas que tocar la función de normalización. Repasa fetch en Fetch y APIs.
Ejercicios
- Crea una interface
Libroconisbn(solo lectura),titulo,autor,anioyresenaopcional. - Crea una interface
Personaconnombreyemail, y unaEmpleadoque la extienda conpuestoysalario. Haz lo mismo contypee intersección. - Define
type Plan = 'basico' | 'pro' | 'empresa'y unRecordcon el precio mensual de cada plan. - Tipa esta respuesta de API y escribe una función que devuelva solo los títulos de los posts publicados:
json
[
{ "id": 1, "title": "Hola", "published": true, "tags": ["intro"] },
{ "id": 2, "title": "Borrador", "published": false, "tags": [] }
]Ver solución
ts
// 1
interface Libro {
readonly isbn: string;
titulo: string;
autor: string;
anio: number;
resena?: string;
}
// 2
interface Persona {
nombre: string;
email: string;
}
interface Empleado extends Persona {
puesto: string;
salario: number;
}
type PersonaT = { nombre: string; email: string };
type EmpleadoT = PersonaT & { puesto: string; salario: number };
// 3
type Plan = 'basico' | 'pro' | 'empresa';
const precios: Record<Plan, number> = {
basico: 99,
pro: 299,
empresa: 999
};
// 4
interface ApiPost {
id: number;
title: string;
published: boolean;
tags: string[];
}
function titulosPublicados(posts: ApiPost[]): string[] {
return posts.filter((p) => p.published).map((p) => p.title);
}Resumen
interfaceytypedescriben la forma de los objetos; TS compara por estructura.?marca propiedades opcionales;readonlyimpide reasignarlas.extendshereda campos en interfaces;&combina tipos (intersección).- Solo
typedefine uniones, tuplas y tipos derivados; solointerfacese fusiona. - Index signatures y
Recorddescriben diccionarios;Recordcon uniones obliga a cubrir todas las claves. - Para APIs, tipa la respuesta original y normalízala a un tipo propio.
Siguiente: Funciones tipadas