Lección 5 de 8. Interfaces, type y clases: dar nombre a la forma de los objetos es lo que más vas a escribir en TypeScript. Aquí está la eterna duda de interface o type, la idea que explica casi todo el lenguaje (el tipado estructural) y cómo se combinan los tipos con las clases de JavaScript.
interface: el nombre de una forma
En la lección 2 diste nombre a la forma de un objeto con type. La otra manera, más habitual para objetos, es interface. Se escribe casi igual, sin el signo igual, y una interfaz puede extender otra para añadirle propiedades:
interface Persona {
nombre: string;
email?: string;
}
interface Empleado extends Persona {
readonly legajo: number;
departamento: string;
}
const e: Empleado = { nombre: "Marta", legajo: 1042, departamento: "Soporte" };
console.log(`${e.nombre} (${e.legajo}) - ${e.departamento}`);Ábrelo en el editor y cámbialo →
salidaMarta (1042) - Soporte
¿interface o type?
Para describir un objeto, las dos sirven y el compilador las trata casi igual. Las diferencias reales son pocas:
interface | type | |
|---|---|---|
| Forma de un objeto | sí | sí |
| Uniones, primitivos, tuplas | no | sí: type Id = number | string |
| Ampliar | extends | intersección & |
| Declararla dos veces | se fusionan en una | error: identificador duplicado |
La última fila es la más curiosa. Si declaras dos veces la misma interfaz, TypeScript junta sus propiedades. Las bibliotecas lo usan para que puedas añadir campos a sus tipos (por ejemplo, a la Window del navegador). Con type, repetir el nombre es un error:
interface Config { puerto: number }
interface Config { host: string } // se fusiona con la anterior
const c: Config = { puerto: 8080, host: "localhost" }; // necesita las dos
type Opciones = { modo: string };
type Opciones = { nivel: number }; // errorÁbrelo en el editor y cámbialo →
tsc --strictfusion.ts(6,6): error TS2300: Identificador 'Opciones' duplicado. fusion.ts(7,6): error TS2300: Identificador 'Opciones' duplicado.
Una regla que funciona: interface para la forma de los objetos, sobre todo los que exporta una biblioteca, y type para todo lo demás: uniones, tuplas, tipos de función y los tipos calculados de las lecciones 6 y 7. Lo importante es ser coherente dentro de un proyecto.
Intersecciones: esto y aquello
Si la unión A | B es «A o B», la intersección A & B es «A y B a la vez»: un objeto que tiene todas las propiedades de los dos. Sirve para componer tipos a partir de piezas:
type ConId = { id: number };
type ConFechas = { creado: Date; modificado?: Date };
type Nota = { titulo: string; texto: string };
type NotaGuardada = Nota & ConId & ConFechas;
const n: NotaGuardada = {
id: 7,
titulo: "Compra",
texto: "pan, leche",
creado: new Date(Date.UTC(2026, 9, 20)),
};
console.log(n.id, n.titulo, n.creado.toISOString().slice(0, 10));Ábrelo en el editor y cámbialo →
salida7 Compra 2026-10-20
Tipado estructural: importa la forma, no el nombre
Esta es la diferencia de fondo con Java o C#. En TypeScript, un valor encaja en un tipo si tiene las propiedades que el tipo pide, se llame como se llame y venga de donde venga. No hace falta declarar nada:
interface Punto { x: number; y: number }
function distanciaAlOrigen(p: Punto): number {
return Math.hypot(p.x, p.y);
}
class Pixel { // no menciona Punto en ningún sitio
x = 3;
y = 4;
color = "rojo";
}
console.log(distanciaAlOrigen(new Pixel())); // tiene x e y: vale
console.log(distanciaAlOrigen({ x: 6, y: 8 }));Ábrelo en el editor y cámbialo →
salida5 10
Es coherente con cómo se usa JavaScript, donde los objetos se crean sobre la marcha sin clases. Pero tiene una consecuencia que hay que conocer: dos tipos con la misma forma son intercambiables. Si Euros y Dolares son los dos number, TypeScript no te protege de mezclarlos:
type Euros = number;
type Dolares = number;
function cobrar(importe: Euros) {
console.log(`Cobrados ${importe} EUR`);
}
const precioEnDolares: Dolares = 50;
cobrar(precioEnDolares); // compila: para TypeScript los dos son numberÁbrelo en el editor y cámbialo →
salidaCobrados 50 EUR
Hay una técnica para distinguirlos, los tipos «con marca», que se explica en las rarezas de TypeScript.
La comprobación de propiedades de más
Que las propiedades de más no molesten tiene una excepción, pensada para cazar erratas: cuando escribes un objeto literal directamente donde se espera un tipo, cualquier propiedad que el tipo no tenga es un error.
interface Opciones { titulo: string; ancho?: number }
function abrirVentana(o: Opciones) {
console.log(o.titulo, o.ancho ?? 800);
}
abrirVentana({ titulo: "Ayuda", anhco: 400 }); // error: errata en ancho
const op = { titulo: "Ayuda", anhco: 400 };
abrirVentana(op); // compila: ya no es un literal escrito ahí mismoÁbrelo en el editor y cámbialo →
tsc --strictde_mas.ts(7,33): error TS2561: El literal de objeto solo puede especificar propiedades conocidas, pero "anhco" no existe en el tipo "Opciones". ¿Quería escribir "ancho"?
Solo hay un error, el de la primera llamada. La segunda pasa porque op es una variable y la regla solo se aplica a literales. Es una de las rarezas de TypeScript que más confunden, y en el ejemplo el resultado es una ventana de 800 píxeles en vez de 400.
Objetos usados como diccionario
Cuando las claves no se conocen de antemano (un recuento de palabras, una caché por identificador), el tipo se escribe con una firma de índice: { [clave: string]: number }, «cualquier clave de texto, con un número como valor». En la lección 6 verás la forma corta, Record<string, number>, y para muchos casos es mejor un Map, que tienes en la lección de arrays y objetos de JavaScript.
const recuento: { [palabra: string]: number } = {};
for (const p of "el gato y el perro y el pez".split(" ")) {
recuento[p] = (recuento[p] ?? 0) + 1;
}
console.log(recuento);Ábrelo en el editor y cámbialo →
salida{ el: 3, gato: 1, y: 2, perro: 1, pez: 1 }Clases con tipos
Las clases de TypeScript son las de JavaScript (las tienes en la lección de clases y this) con tipos en los campos y tres modificadores de visibilidad: public (por omisión), private (solo dentro de la clase) y protected (también en las subclases).
class Cuenta {
private saldo = 0;
#pin: string;
constructor(readonly titular: string, pin: string) {
this.#pin = pin;
}
ingresar(importe: number): void {
if (importe <= 0) throw new Error("El importe debe ser positivo");
this.saldo += importe;
}
get resumen(): string {
return `${this.titular}: ${this.saldo} EUR`;
}
}
const c = new Cuenta("Ana", "1234");
c.ingresar(150);
console.log(c.resumen);
c.saldo = 1000000; // error: es privateÁbrelo en el editor y cámbialo →
tsc --strictclase.ts(22,3): error TS2341: La propiedad 'saldo' es privada y solo se puede acceder a ella en la clase 'Cuenta'.
private frente a #
En la clase de arriba hay dos campos privados escritos de forma distinta, y no son lo mismo. private es de TypeScript: lo comprueba el compilador y se borra al ejecutar, así que en ejecución el campo es tan público como cualquier otro. #pin es de JavaScript, y es privado de verdad: ni siquiera se ve desde fuera.
class Cuenta {
private saldo = 150;
#pin = "1234";
}
const c = new Cuenta();
console.log(JSON.stringify(c)); // saldo aparece; #pin no
console.log(Object.keys(c));
console.log((c as any).saldo); // se puede leer saltándose el compiladorÁbrelo en el editor y cámbialo →
salida{"saldo":150}
[ 'saldo' ]
150Si lo privado tiene que serlo de verdad (un secreto, un estado que nadie debe tocar), usa #. Si solo quieres ordenar la API de la clase para quien la usa, private basta.
Lo que Node no ejecuta: propiedades de parámetro
En el constructor de Cuenta hay un atajo: readonly titular: string como parámetro declara el campo y lo asigna a la vez. Se llama propiedad de parámetro, y es de las pocas construcciones de TypeScript que generan código propio, como los enum de la lección 1. Por eso Node, que solo borra tipos, la rechaza:
class Punto {
constructor(public x: number, public y: number) {}
}
console.log(new Punto(1, 2));node prop_parametro.tsSyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode
Si ejecutas los .ts directamente con Node, declara los campos a la vista y asígnalos en el constructor. Con tsc como compilador (o con herramientas como Vite o esbuild) el atajo funciona, y la opción --erasableSyntaxOnly te avisa si quieres evitarlo.
implements: que una clase cumpla un contrato
El error que te ahorra: una clase a la que le falta un método
class Circulo {
constructor(r) { this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
class Cuadrado {
constructor(lado) { this.lado = lado; }
// se nos olvidó area()
}
for (const f of [new Circulo(1), new Cuadrado(2)]) {
console.log(f.area().toFixed(2));
}node implements_antes.mjs3.14 TypeError: f.area is not a function
El primer área se imprime y el programa revienta en el segundo. Con TypeScript, la clase declara que cumple una interfaz con implements, y el olvido salta en la propia clase, antes de que nadie la use:
interface Figura {
area(): number;
}
class Circulo implements Figura {
constructor(private r: number) {}
area() { return Math.PI * this.r ** 2; }
}
class Cuadrado implements Figura {
constructor(private lado: number) {}
}tsc --strictimplements.ts(10,7): error TS2420: La clase 'Cuadrado' implementa la interfaz 'Figura' de forma incorrecta. La propiedad "area" falta en el tipo "Cuadrado", pero es obligatoria en el tipo "Figura".
Clases abstractas
Una clase abstract no se puede instanciar: sirve de base para otras. Puede tener métodos ya escritos, que heredan las subclases, y métodos abstract, sin cuerpo, que cada subclase está obligada a escribir:
abstract class Figura {
abstract area(): number;
describir(): string {
return `${this.constructor.name} de área ${this.area().toFixed(2)}`;
}
}
class Cuadrado extends Figura {
constructor(private lado: number) { super(); }
area() { return this.lado ** 2; }
}
console.log(new Cuadrado(3).describir());
const f = new Figura(); // errorÁbrelo en el editor y cámbialo →
tsc --strictabstracta.ts(14,11): error TS2511: No se puede crear una instancia de una clase abstracta.
¿Interfaz o clase abstracta? Si solo quieres un contrato, una interfaz: no genera código y una clase puede implementar varias. La clase abstracta tiene sentido cuando además quieres compartir código entre las subclases.
Resumen
| Para | Escribe | Recuerda |
|---|---|---|
| La forma de un objeto | interface X { ... } | se puede ampliar con extends |
| Uniones, tuplas, funciones | type X = ... | no se fusiona |
| Juntar tipos | A & B | tiene todo lo de los dos |
| Un diccionario | { [k: string]: number } | o Record, o un Map |
| Privado para el compilador | private | se borra al ejecutar |
| Privado de verdad | #campo | es JavaScript |
| Un contrato para clases | implements | el fallo sale en la clase |
| Una base con código común | abstract class | no se puede instanciar |
Retos
Reto 1. Escribe una interfaz Producto (nombre y precio) y otra ProductoEnOferta que la extienda con un descuento entre 0 y 1. Escribe precioFinal(p: Producto | ProductoEnOferta) usando in de la lección 3.
Ver la solución
interface Producto { nombre: string; precio: number }
interface ProductoEnOferta extends Producto { descuento: number }
function precioFinal(p: Producto | ProductoEnOferta): number {
return "descuento" in p ? p.precio * (1 - p.descuento) : p.precio;
}
console.log(precioFinal({ nombre: "Taza", precio: 10 }));
console.log(precioFinal({ nombre: "Vaso", precio: 10, descuento: 0.25 }));Ábrelo en el editor y cámbialo →
salida10 7.5
Reto 2. ¿Compila esto? interface A { x: number }, const b = { x: 1, y: 2 };, const a: A = b;. ¿Y si escribes const a: A = { x: 1, y: 2 };?
Ver la solución
interface A { x: number }
const b = { x: 1, y: 2 };
const a1: A = b; // compila: estructural, y b no es un literal aquí
const a2: A = { x: 1, y: 2 }; // error: comprobación de propiedades de másÁbrelo en el editor y cámbialo →
tsc --strictreto2.ts(4,23): error TS2353: El literal de objeto solo puede especificar propiedades conocidas y 'y' no existe en el tipo 'A'.
Reto 3. Escribe una clase Contador con un campo realmente privado (que no aparezca en JSON.stringify), un método sumar() y un get valor. Comprueba que el JSON sale vacío.
Ver la solución
class Contador {
#n = 0;
sumar(): void { this.#n++; }
get valor(): number { return this.#n; }
}
const c = new Contador();
c.sumar(); c.sumar();
console.log(c.valor, JSON.stringify(c));Ábrelo en el editor y cámbialo →
salida2 {}Todo el código de esta lección se comprobó con tsc 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. ¿Quieres probar algo? Usa el editor de TypeScript.
