learning-front

Nivel 5 · TypeScript: JavaScript con red de seguridad

Interfaces y type aliases

Modelar la forma de los objetos con interface y type, propiedades opcionales y de solo lectura, y cuándo usar cada uno.

En el capítulo anterior tipamos valores sueltos: un nombre es string, unas partidas son number. Eso ya ayuda, pero en un proyecto real casi todo lo interesante vive dentro de objetos: un héroe tiene nombre, rol, partidas y victorias. Si TypeScript no conoce la forma de ese objeto, no puede avisarte cuando accedes a un campo que no existe o le pasas el objeto equivocado a una función.

Para eso existen las interfaces y los type aliases: herramientas para describir la forma de los objetos y darle nombres a esos tipos, de modo que puedas reutilizarlos en todo el código.

interface: describir la forma de un objeto#

Una interface es un contrato de forma. Le dices a TypeScript “cualquier objeto de este tipo tendrá exactamente estos campos, con estos tipos”:

typescript
// interface describe qué campos tiene el objeto y de qué tipo es cada uno.
interface Heroe {
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
}

A partir de ese momento, puedes usar Heroe como tipo:

typescript
// TypeScript comprueba que el objeto cumple la forma.
const tracer: Heroe = {
  nombre: "Tracer",
  rol: "Daño",
  partidas: 120,
  victorias: 78,
};

Si el objeto no cumple la forma —le falta un campo, tiene uno de más, o un campo es del tipo equivocado— TypeScript lo marca en rojo antes de ejecutar. Descomenta las líneas del editor para ver los errores en el panel “Problemas”.

type alias: otro nombre para cualquier tipo#

type también sirve para describir objetos, con una sintaxis casi idéntica:

typescript
// type alias para un objeto: la forma entre llaves, igual que interface.
type Heroe = {
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
};

La diferencia práctica es que type sirve para nombrar cualquier tipo, no solo objetos. Por ejemplo, un identificador numérico:

typescript
// Darle nombre a un tipo primitivo: las firmas quedan más expresivas.
type IdHeroe = number;

Cuando uses IdHeroe en la firma de una función, quien lea el código sabrá que ese number es un identificador, no una cantidad cualquiera.

¿Cuándo usar cada uno? La convención en TypeScript es usar interface para describir objetos y APIs (se puede extender con extends, lo que facilita componer tipos en capas), y type para darle nombre a tipos que no son objetos —un primitivo como IdHeroe, o composiciones más complejas que veremos en capítulos futuros—. En la práctica, para objetos simples los dos funcionan igual.

Propiedades opcionales con ?#

No todos los objetos tienen los mismos campos. Algunos héroes tienen apodo, otros no. Marcar un campo como opcional con ? le dice a TypeScript que puede estar o no estar:

typescript
interface Heroe {
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
  // apodo puede estar o no estar; si quieres usarlo, comprueba primero si existe.
  apodo?: string;
}

Un objeto sin apodo sigue siendo un Heroe válido. TypeScript no protesta. Pero cuando accedes al campo, te recuerda que puede ser undefined: si quieres usarlo, conviene comprobar primero si existe.

Campos de solo lectura con readonly#

Algunos campos no deberían cambiar una vez creado el objeto. El nombre y el rol de un héroe son datos de origen: si algo los sobreescribe, es casi siempre un bug. readonly lo convierte en un error de compilación:

typescript
interface Heroe {
  // nombre y rol no se pueden reasignar: son la identidad del héroe.
  readonly nombre: string;
  readonly rol: string;
  // partidas y victorias sí se actualizan con cada partida.
  partidas: number;
  victorias: number;
}

Si algo intenta reasignar un campo readonly, TypeScript lo marca antes de ejecutar. Al compilar a JavaScript, readonly desaparece —como todos los tipos—, pero su valor está en pillarte el error en desarrollo, no en producción.

Componer interfaces con extends#

A medida que el dominio crece, conviene separar responsabilidades. extends permite crear una interface que hereda todos los campos de otra y añade los suyos:

typescript
// La identidad: lo que define quién es el héroe.
interface HeroeBase {
  readonly nombre: string;
  readonly rol: string;
}

// Las estadísticas: extiende la base y añade los campos de juego.
interface HeroeConStats extends HeroeBase {
  partidas: number;
  victorias: number;
}

Un objeto de tipo HeroeConStats cumple también HeroeBase (tiene todos sus campos), así que puedes pasárselo a cualquier función que pida la base. Al revés no: un HeroeBase no tiene las estadísticas, así que no puedes usarlo donde se pida HeroeConStats.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué describe una `interface` en TypeScript?

Tu turno#

Tienes el array del equipo sin tipar: TypeScript no conoce la forma de los objetos y el panel “Problemas” lo indica. Define la interface, anótala donde toca y deja el panel vacío. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Modela el equipo del Team Builder

Partes de un fichero con objetos de héroe sin tipar: TypeScript no conoce su forma y el panel "Problemas" lo indica. Define la interface que describe un héroe y úsala para tipar el array y las funciones.

Paso 1: Que funcione

  • Defines una interface con los cinco campos del héroe (id, nombre, rol, partidas, victorias).
  • El array y el parámetro de la función quedan anotados con ese tipo.
  • El panel "Problemas" queda vacío y la consola muestra el resumen.
Ver soluciones
// Solución OK: una interface que describe el héroe y resuelve el problema.
// Funciona y compila limpio; es la traducción más directa del enunciado.

// Una interface con los cinco campos que tiene un héroe.
interface Heroe {
  id: number;
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
}

// El array queda tipado: TypeScript ya sabe que cada elemento es un Heroe.
const equipo: Heroe[] = [
  { id: 1, nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
  { id: 2, nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
  { id: 3, nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
];

// El parámetro anotado: TypeScript ya sabe que heroe.victorias y heroe.partidas existen.
function winrate(heroe: Heroe): number {
  return heroe.victorias / heroe.partidas;
}

// El parámetro y el retorno anotados.
function formatearPorcentaje(ratio: number): string {
  return (ratio * 100).toFixed(1) + "%";
}

// Pinta el resumen del equipo.
equipo.forEach(function (heroe) {
  console.log(
    heroe.nombre +
      " (" +
      heroe.rol +
      "): " +
      formatearPorcentaje(winrate(heroe)),
  );
});

Por qué este nivel

  • Define una interface con los cinco campos y la usa para tipar el array y el parámetro: traducción directa del enunciado, compila y funciona.
  • El panel "Problemas" queda vacío: ya es suficiente para llamarlo "correcto".
  • Su límite: todos los campos son mutables (cualquiera puede reasignar nombre o rol sin que TypeScript proteste) y no hay distinción entre campos de identidad y campos de juego.