Diferencias entre `type` e `interface` al definir tipos personalizados en TypeScript

TypeScript ofrece dos construcciones principales para modelar tipos propios: type e interface. Aunque ambas pueden describir la forma de un objeto, sus capacidades y su comportamiento en tiempo de compilación no son idénticos.

type: alias de tipos

Un alias creado con type asigna un nuevo nombre a cualquier tipo válido: primitivos, objetos, uniones, intersecciones, tuplas, funciones, tipos condicionales y tipos mapeados.

type IdUsuario = string | number;
type Activo = boolean;
type Coordenada = [number, number];

type Usuario = {
  id: IdUsuario;
  nombre: string;
  edad: number;
  activo: Activo;
};

type RespuestaApi<T> =
  | { ok: true; datos: T }
  | { ok: false; error: string };

interface: contratos de forma

interface define la estructura que debe cumplir un objeto o una clase. Su sintaxis está orientada a describir la forma de entidades y a establecer contratos en programación orientada a objetos.

interface Usuario {
  id: string | number;
  nombre: string;
  edad: number;
  activo: boolean;
}

interface Servicio {
  ejecutar(): void;
}

Similitudes

Describen objetos y funciones

Ambas pueden modelar objetos y firmas de función, aunque la sintaxis difiere.

type PuntoType = {
  x: number;
  y: number;
};

interface PuntoInterface {
  x: number;
  y: number;
}

type ComparadorType = (a: number, b: number) => number;

interface ComparadorInterface {
  (a: number, b: number): number;
}

Compatibilidad entre ambas

Las interfaces y los alias no son mutuamente excluyentes. Una interfaz puede extender un alias y viceversa.

type Base = {
  nombre: string;
};

interface Empleado extends Base {
  departamento: string;
}

interface Persona {
  nombre: string;
}

type Cliente = Persona & {
  credito: number;
};

Implementación en clases

Una clase puede implementar tanto una interfaz como un alias de tipo objeto. La excepción son los alias de unión, que no pueden implementarse.

interface Volador {
  volar(): void;
}

type Nadador = {
  nadar(): void;
};

class Pato implements Volador, Nadador {
  volar() {}
  nadar() {}
}

type Serializable =
  | { serializar(): string }
  | { toJSON(): string };

// Error: una clase no puede implementar una unión de tipos.
// class Cache implements Serializable {}

Diferencias principales

1. Alias de tipos primitivos

type puede renombrar tipos primitivos; interface no admite esta operación.

type Nombre = string;
type Identificador = number;
type UUID = `${string}-${string}-${string}-${string}-${string}`;

2. Uniones, intersecciones y tuplas

Los alias permiten definir uniones, intersecciones y tuplas. Las interfaces no expresan directamente uniones ni tuplas.

type EstadoPedido = 'pendiente' | 'procesando' | 'enviado';
type Coordenada = [latitud: number, longitud: number];

type UsuarioConPermisos = Usuario & {
  permisos: string[];
};

type Resultado = { exito: true; valor: number } | { exito: false; mensaje: string };

3. Fusión de declaraciones

Si se declara varias veces una interfaz con el mismo identificador en el mismo ámbito, TypeScript las fusiona en una sola. Los alias no permiten esta fusión.

interface Configuracion {
  host: string;
}

interface Configuracion {
  puerto: number;
}

const c: Configuracion = {
  host: 'localhost',
  puerto: 3000,
};

// Error: no se puede declarar dos veces el mismo alias.
// type Configuracion = { host: string };
// type Configuracion = { puerto: number };

4. Comportamiento con firmas de índice

Un objeto definido mediante interface no se puede asignar directamente a una firma de índice, mientras que uno definido con type sí. Esto se debe a que las interfaces son abiertas a la fusión de declaraciones y, por tanto, sus propiedades no se consideran completamente conocidas.

interface Diccionario {
  [clave: string]: string;
}

type TituloType = {
  titulo: string;
};

interface TituloInterface {
  titulo: string;
}

const a: TituloType = { titulo: 'Inicio' };
const b: TituloInterface = { titulo: 'Inicio' };

const dict: Diccionario = a; // OK
// const dict2: Diccionario = b; // Error: falta la firma de índice.

Cuándo usar cada uno

  • Prefiere interface cuando describas la forma de objetos, APIs públicas, contratos de clases o cuando esperes ampliar la definición mediante fusión de declaraciones.
  • Prefiere type para alias de primitivos, uniones, intersecciones, tuplas, tipos mapeados, tipos condicionales y cualquier tipo que resulte de una composición más compleja.

Ejemplo práctico

interface Producto {
  nombre: string;
  precio: number;
  sku: string;
}

type Descuento =
  | { tipo: 'porcentaje'; valor: number }
  | { tipo: 'fijo'; valor: number };

function calcularTotal(producto: Producto, descuento: Descuento): number {
  if (descuento.tipo === 'porcentaje') {
    return producto.precio * (1 - descuento.valor / 100);
  }
  return Math.max(0, producto.precio - descuento.valor);
}

const portatil: Producto = {
  nombre: 'Portátil',
  precio: 1500,
  sku: 'P-001',
};

const rebaja: Descuento = { tipo: 'porcentaje', valor: 10 };
console.log(calcularTotal(portatil, rebaja)); // 1350

En la mayoría de los casos, ambas construcciones pueden usarse indistintamente para objetos simples. Sin embargo, elegir la adecuada mejora la claridad del código y evita limitaciones en escenarios avanzados.

Etiquetas: TypeScript Type Aliases interfaces Declaration Merging Static Typing

Publicado el 7-22 10:08