Objetos e interfaces en TypeScript
En esta lección
- De la forma en línea a la interfaz
- Lo que comprueba TypeScript
- Propiedades opcionales
- Propiedades de solo lectura
- Objetos anidados y arrays de objetos
- Métodos en una interfaz
- Objetos con claves libres: firmas de índice
- Ampliar interfaces con extends
- Tipado estructural
- Las interfaces se fusionan
- ¿interface o type?
- Errores frecuentes
- Resumen
En JavaScript casi todo son objetos: un usuario, un pedido, la configuración de una app, la respuesta de una API. En TypeScript describes la forma de esos objetos (qué propiedades tienen y de qué tipo) para que el compilador vigile que los usas bien. En esta lección aprenderás a hacerlo con interfaces: propiedades opcionales y de solo lectura, objetos anidados, métodos, objetos con claves libres y cómo una interfaz puede ampliar otra.
De la forma en línea a la interfaz
En Tipos básicos viste que puedes escribir la forma de un objeto directamente en la anotación:
const socio: { nombre: string; cuota: number; activo: boolean } = {
nombre: "Irene",
cuota: 35,
activo: true,
};
console.log(socio.nombre);
// Irene
Funciona, pero si tienes diez socios, una función que recibe un socio y otra que lo devuelve, tendrías que repetir esa forma en cada sitio. Una interfaz le pone nombre a esa forma para reutilizarla:
interface Socio {
nombre: string;
cuota: number;
activo: boolean;
}
const irene: Socio = { nombre: "Irene", cuota: 35, activo: true };
const pablo: Socio = { nombre: "Pablo", cuota: 40, activo: false };
function describir(s: Socio): string {
return `${s.nombre} paga ${s.cuota} € (${s.activo ? "activo" : "de baja"})`;
}
console.log(describir(irene));
console.log(describir(pablo));
// Irene paga 35 € (activo)
// Pablo paga 40 € (de baja)
- Se escribe
interface, el nombre en PascalCase (cada palabra con mayúscula inicial:Socio,LineaPedido) y las propiedades entre llaves. - Cada propiedad lleva su tipo. Puedes separarlas con
;, con,o solo con saltos de línea; lo habitual es;. - Después usas el nombre como cualquier otro tipo:
Socio,Socio[],(s: Socio) => void…
Nota: las interfaces solo existen para el compilador. Al pasar a JavaScript desaparecen por completo: no generan ni una línea de código.
Lo que comprueba TypeScript
Con una interfaz, TypeScript avisa si falta una propiedad, si tiene el tipo equivocado o si intentas leer una que no existe:
interface Socio {
nombre: string;
cuota: number;
activo: boolean;
}
const lucia: Socio = { nombre: "Lucía", cuota: "35", activo: true };
// Error: Type 'string' is not assignable to type 'number'.
const mario: Socio = { nombre: "Mario", cuota: 40 };
// Error: Property 'activo' is missing in type '{ nombre: string; cuota: number; }'
// but required in type 'Socio'.
const sara: Socio = { nombre: "Sara", cuota: 30, activo: true };
console.log(sara.email);
// Error: Property 'email' does not exist on type 'Socio'.
Propiedades de más
Si escribes un objeto directamente donde se espera un Socio y le pones una propiedad que la interfaz no tiene, también es un error. Es la comprobación de propiedades sobrantes y sirve sobre todo para cazar erratas:
interface Socio {
nombre: string;
cuota: number;
activo: boolean;
}
const ana: Socio = { nombre: "Ana", cuota: 35, activo: true, cuotta: 40 };
// Error: Object literal may only specify known properties, but 'cuotta' does not
// exist in type 'Socio'. Did you mean to write 'cuota'?
TypeScript incluso adivina qué querías escribir.
Esta comprobación solo se hace con objetos escritos en ese momento (literales). Si el objeto viene de otra variable con más propiedades, TypeScript lo acepta, como verás en el apartado de tipado estructural.
Propiedades opcionales
No todos los datos son obligatorios. Una propiedad que puede faltar se marca con ? detrás del nombre:
interface Producto {
nombre: string;
precio: number;
descuento?: number; // puede no estar
}
const mochila: Producto = { nombre: "Mochila 30 L", precio: 59.9 };
const botas: Producto = { nombre: "Botas de montaña", precio: 120, descuento: 15 };
function precioFinal(p: Producto): number {
const rebaja = p.descuento ?? 0;
return p.precio * (1 - rebaja / 100);
}
console.log(precioFinal(mochila), precioFinal(botas));
// 59.9 102
Una propiedad opcional tiene tipo number | undefined, así que TypeScript no te deja usarla directamente en una operación sin tener en cuenta que puede faltar:
interface Producto {
nombre: string;
precio: number;
descuento?: number;
}
function ahorro(p: Producto): number {
return p.precio * p.descuento / 100;
}
// Error: 'p.descuento' is possibly 'undefined'.
Las soluciones habituales son las que ya usaste arriba: un valor por defecto con ??, una comprobación con if (p.descuento !== undefined) o el encadenamiento opcional ?. si la propiedad es un objeto.
Consejo: no conviertas todo en opcional “por si acaso”. Cada
?te obliga a comprobar la propiedad en todos los sitios donde la uses. Márcala opcional solo si de verdad puede faltar.
Propiedades de solo lectura
Con readonly delante del nombre, la propiedad se puede dar al crear el objeto, pero no cambiarse después. Es ideal para identificadores, fechas de alta o cualquier dato que no deba tocarse:
interface Pedido {
readonly id: string;
cliente: string;
enviado: boolean;
}
const pedido: Pedido = { id: "P-1043", cliente: "Nerea", enviado: false };
pedido.enviado = true; // bien
pedido.id = "P-9999";
// Error: Cannot assign to 'id' because it is a read-only property.
readonly es parecido a const, pero para propiedades: const impide reasignar una variable; readonly, una propiedad. Igual que todos los tipos, solo protege al compilar: en el JavaScript resultante la propiedad se puede cambiar.
Cuidado:
readonlyes superficial. Si la propiedad es un array u objeto, no puedes sustituirlo por otro, pero sí modificar su contenido (pedido.lineas.push(...)). Para proteger también el contenido, usareadonly string[], como viste en Arrays y tuplas.
Objetos anidados y arrays de objetos
Una propiedad puede ser otro objeto, un array o un array de objetos. Lo más claro es crear una interfaz para cada pieza y combinarlas:
interface Direccion {
calle: string;
ciudad: string;
cp: string;
}
interface LineaPedido {
producto: string;
unidades: number;
precio: number;
}
interface Pedido {
readonly id: string;
cliente: string;
envio: Direccion;
lineas: LineaPedido[];
notas?: string;
}
const pedido: Pedido = {
id: "P-2001",
cliente: "Nerea",
envio: { calle: "Calle Alta 12", ciudad: "Santander", cp: "39008" },
lineas: [
{ producto: "Cuerda 60 m", unidades: 1, precio: 140 },
{ producto: "Mosquetón", unidades: 4, precio: 8.5 },
],
};
const total = pedido.lineas.reduce((suma, l) => suma + l.unidades * l.precio, 0);
console.log(`Pedido ${pedido.id} para ${pedido.envio.ciudad}: ${total} €`);
console.log(pedido.notas?.toUpperCase() ?? "Sin notas");
// Pedido P-2001 para Santander: 174 €
// Sin notas
Dividir en interfaces pequeñas tiene otra ventaja: puedes reutilizar Direccion en clientes, almacenes o tiendas.
Métodos en una interfaz
Una interfaz también puede describir funciones que forman parte del objeto. Hay dos formas de escribirlo, y las dos significan prácticamente lo mismo:
interface Contador {
valor: number;
sumar(cantidad: number): void; // sintaxis de método
reiniciar: () => void; // propiedad que guarda una función
}
const visitas: Contador = {
valor: 0,
sumar(cantidad) {
this.valor += cantidad;
},
reiniciar() {
this.valor = 0;
},
};
visitas.sumar(3);
visitas.sumar(2);
console.log(visitas.valor);
visitas.reiniciar();
console.log(visitas.valor);
// 5
// 0
Fíjate en que dentro del objeto no hace falta escribir el tipo de cantidad: TypeScript lo saca de la interfaz. Cómo se tipan las funciones en detalle lo verás en Funciones.
Las interfaces con métodos son también la base de las clases: con implements una clase promete cumplir una interfaz. Lo verás en Clases.
Objetos con claves libres: firmas de índice
A veces no sabes de antemano qué propiedades tendrá un objeto, pero sí de qué tipo son todas. Por ejemplo, el stock de un almacén, donde cada clave es un código de producto. Para eso se usa una firma de índice:
interface Stock {
[codigo: string]: number;
}
const almacen: Stock = {
"CUE-60": 4,
"MOS-01": 120,
};
almacen["ARN-02"] = 15;
almacen["PIE-44"] = "diez";
// Error: Type 'string' is not assignable to type 'number'.
[codigo: string]: number se lee “cualquier clave de texto tiene un valor numérico”. El nombre codigo es solo descriptivo; podría ser clave o k.
Puedes combinar propiedades fijas y una firma de índice, siempre que las fijas sean compatibles con ella:
interface Notas {
alumno: string;
[asignatura: string]: string | number;
}
const notasDeAitor: Notas = {
alumno: "Aitor",
programacion: 8,
entornos: 6.5,
};
for (const [clave, valor] of Object.entries(notasDeAitor)) {
console.log(clave, valor);
}
// alumno Aitor
// programacion 8
// entornos 6.5
Nota: para objetos tipo diccionario también se usa mucho el tipo utilitario
Record<string, number>, que equivale a la firma de índice. Lo verás en Tipos utilitarios. Y si las claves se añaden y quitan constantemente, a menudo es mejor unMap(repásalo en Set y Map).
Ampliar interfaces con extends
Una interfaz puede extender otra: hereda todas sus propiedades y añade las suyas. Evita repetir campos comunes:
interface Persona {
nombre: string;
email: string;
}
interface Alumno extends Persona {
curso: string;
notas: number[];
}
interface Profesor extends Persona {
departamento: string;
}
const alumna: Alumno = {
nombre: "Carla",
email: "carla@instituto.es",
curso: "1.º DAW",
notas: [7, 8.5],
};
const profe: Profesor = { nombre: "Jon", email: "jon@instituto.es", departamento: "Informática" };
function saludar(p: Persona): string {
return `Hola, ${p.nombre}`;
}
console.log(saludar(alumna), "/", saludar(profe));
// Hola, Carla / Hola, Jon
Fíjate en que saludar pide una Persona y acepta tanto un Alumno como un Profesor: los dos tienen, como mínimo, todo lo que tiene una Persona.
Una interfaz puede extender varias a la vez, separadas por comas:
interface ConId {
readonly id: number;
}
interface ConFechas {
creado: Date;
actualizado?: Date;
}
interface Articulo extends ConId, ConFechas {
titulo: string;
}
const post: Articulo = { id: 1, creado: new Date("2026-03-01"), titulo: "Primeros pasos" };
console.log(post.id, post.titulo, post.creado.getFullYear());
// 1 Primeros pasos 2026
Tipado estructural
En Java o C#, un objeto es de un tipo porque lo declaras así. TypeScript funciona de otra forma: un objeto es compatible con una interfaz si tiene la forma que pide, se llame como se llame. Es el tipado estructural (también llamado “tipado de pato”: si camina como un pato y hace cua, es un pato).
interface ConNombre {
nombre: string;
}
function etiqueta(x: ConNombre): string {
return `[${x.nombre}]`;
}
const perro = { nombre: "Toby", raza: "Beagle" };
const ruta = { nombre: "Ruta del Cares", km: 12 };
console.log(etiqueta(perro), etiqueta(ruta));
// [Toby] [Ruta del Cares]
Ni perro ni ruta dicen en ningún sitio que sean ConNombre, pero los dos tienen una propiedad nombre de tipo string, y eso basta. Las propiedades de más (raza, km) no molestan porque no son literales escritos en la llamada; si escribieras etiqueta({ nombre: "Toby", raza: "Beagle" }), saltaría la comprobación de propiedades sobrantes.
Gracias a esto, las funciones pueden pedir solo lo que necesitan y servir para muchos tipos de objetos distintos.
Las interfaces se fusionan
Una peculiaridad: si declaras dos veces una interfaz con el mismo nombre, TypeScript las fusiona en una sola con las propiedades de ambas.
interface Ajustes {
tema: string;
}
interface Ajustes {
idioma: string;
}
const misAjustes: Ajustes = { tema: "oscuro", idioma: "es" };
console.log(misAjustes);
// { tema: 'oscuro', idioma: 'es' }
Se usa sobre todo para ampliar tipos de librerías (por ejemplo, añadir una propiedad propia a Window). En tu propio código es mejor no repetir nombres: si ocurre por accidente, el error puede ser difícil de entender.
¿interface o type?
Pronto verás que también puedes describir un objeto con type:
type Coordenada = {
lat: number;
lon: number;
};
const faro: Coordenada = { lat: 43.49, lon: -3.79 };
console.log(faro.lat);
// 43.49
Para objetos, interface y type son casi intercambiables. La diferencia principal es que type puede describir cualquier tipo (uniones, tuplas, funciones) y interface solo objetos. Encontrarás la comparativa completa en Uniones y alias. Una regla práctica muy extendida: interface para la forma de los objetos y type para todo lo demás.
Errores frecuentes
- Escribir la interfaz con minúscula o con prefijo
I: la convención actual es PascalCase sin prefijo:Socio, nosocioniISocio. - Separar propiedades con
=: dentro de una interfaz se escribenombre: string, nonombre = string. No es un objeto, es una descripción. - Marcar todo como opcional: cada
?obliga a comprobar la propiedad. Usa?solo para datos que de verdad pueden faltar. - Usar una opcional sin comprobarla:
p.descuento * 2da error porque puede serundefined. Usa??, unifo?.. - Creer que
readonlyprotege al ejecutar: solo lo comprueba el compilador, y además es superficial con arrays y objetos. - Sorprenderse con la comprobación de propiedades sobrantes: solo salta con objetos literales; casi siempre indica una errata.
- Pensar que las interfaces existen en JavaScript: no puedes hacer
if (x instanceof Socio)con una interfaz, porque desaparece al compilar. Cómo comprobar tipos al ejecutar lo verás en Estrechar tipos.
Resumen
| Concepto | Ejemplo |
|---|---|
| Interfaz | interface Socio { nombre: string; cuota: number } |
| Usarla | const s: Socio = {...}, Socio[], (s: Socio) => ... |
| Opcional | descuento?: number (es number | undefined) |
| Solo lectura | readonly id: string |
| Anidados | envio: Direccion; lineas: LineaPedido[] |
| Métodos | sumar(n: number): void o sumar: (n: number) => void |
| Firma de índice | [codigo: string]: number |
| Extender | interface Alumno extends Persona { ... } |
| Tipado estructural | Vale cualquier objeto con la forma pedida |
| Fusión | Dos interface con el mismo nombre se combinan |
En la siguiente lección aprenderás a combinar tipos con | y a dar nombre a cualquier tipo con type: Uniones y alias.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.