Skip to content

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)); // 790

Mé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 usuario al Request de 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 identifier

Buena 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 ​

  1. Crea una interface Libro con isbn (solo lectura), titulo, autor, anio y resena opcional.
  2. Crea una interface Persona con nombre y email, y una Empleado que la extienda con puesto y salario. Haz lo mismo con type e intersección.
  3. Define type Plan = 'basico' | 'pro' | 'empresa' y un Record con el precio mensual de cada plan.
  4. 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 ​

  • interface y type describen la forma de los objetos; TS compara por estructura.
  • ? marca propiedades opcionales; readonly impide reasignarlas.
  • extends hereda campos en interfaces; & combina tipos (intersección).
  • Solo type define uniones, tuplas y tipos derivados; solo interface se fusiona.
  • Index signatures y Record describen diccionarios; Record con uniones obliga a cubrir todas las claves.
  • Para APIs, tipa la respuesta original y normalízala a un tipo propio.

Siguiente: Funciones tipadas

Academia Web — aprende, copia y construye.