Type vs interface

Intermedio
TypeScript
TypeScript
Actualizado: 18/04/2026

Sintaxis y capacidades básicas

Tanto interface como type pueden describir la forma de un objeto en TypeScript. La diferencia fundamental es que interface está diseñada específicamente para describir formas de objetos, mientras que type es un alias que puede dar nombre a cualquier tipo.

Comparación entre type e interface

// Con interface
interface UsuarioI {
  nombre: string
  edad: number
  activo: boolean
}

// Con type
type UsuarioT = {
  nombre: string
  edad: number
  activo: boolean
}

// Ambos funcionan de forma identica para tipar objetos
const usuario1: UsuarioI = { nombre: "Ana", edad: 30, activo: true }
const usuario2: UsuarioT = { nombre: "Carlos", edad: 25, activo: false }

Cuando ambos describen la misma forma de objeto, son estructuralmente compatibles. Un valor tipado con UsuarioI puede asignarse a una variable de tipo UsuarioT y viceversa.

const u: UsuarioI = { nombre: "Elena", edad: 28, activo: true }
const t: UsuarioT = u // Compatible: misma forma

Lo que solo type puede hacer

Los alias de tipo pueden nombrar cualquier tipo, no solo formas de objeto:

// Alias para tipos primitivos
type ID = string | number
type Moneda = "EUR" | "USD" | "GBP"

// Alias para tuplas
type Coordenada = [number, number]
type RGB = [number, number, number]

// Alias para tipos de función
type Manejador = (evento: string) => void
type Predicado<T> = (valor: T) => boolean

// Alias para tipos condicionales
type Nullable<T> = T | null
type NonNullableCustom<T> = T extends null | undefined ? never : T

const id: ID = 42
const moneda: Moneda = "EUR"
const punto: Coordenada = [10, 20]
const esPositivo: Predicado<number> = (n) => n > 0

console.log(id)              // 42
console.log(moneda)          // "EUR"
console.log(punto)           // [10, 20]
console.log(esPositivo(5))   // true

Ninguna de estas definiciones es posible con interface. Las interfaces solo pueden describir formas de objetos y call/construct signatures.

Declaration merging: exclusivo de interfaces

La declaration merging es una capacidad que solo tienen las interfaces. Cuando se declaran dos interfaces con el mismo nombre, TypeScript las fusiona automáticamente:

interface Config {
  apiUrl: string
}

interface Config {
  timeout: number
}

interface Config {
  debug?: boolean
}

// Config ahora tiene apiUrl, timeout y debug
const config: Config = {
  apiUrl: "https://api.ejemplo.com",
  timeout: 3000,
  debug: true
}

console.log(config.apiUrl)   // "https://api.ejemplo.com"
console.log(config.timeout)  // 3000

Con type, declarar el mismo nombre dos veces produce un error:

type Punto = { x: number }
// Error: Duplicate identifier 'Punto'
// type Punto = { y: number }

Uso práctico del merging

Declaration merging es útil para ampliar tipos de bibliotecas externas o definiciones globales sin modificar el código fuente:

// Definición original de una biblioteca
interface Respuesta {
  código: number
  mensaje: string
}

// Ampliacion en tu código
interface Respuesta {
  timestamp: Date
  versión: string
}

const respuesta: Respuesta = {
  código: 200,
  mensaje: "OK",
  timestamp: new Date(),
  versión: "2.0"
}

console.log(respuesta.código)    // 200
console.log(respuesta.versión)   // "2.0"

Composición: extends vs intersección

Interfaces con extends

Las interfaces usan extends para heredar de otras interfaces. El compilador verifica que no haya conflictos entre las propiedades heredadas:

interface Animal {
  nombre: string
  edad: number
}

interface Mascota extends Animal {
  dueno: string
  vacunada: boolean
}

interface Perro extends Mascota {
  raza: string
}

const perro: Perro = {
  nombre: "Rex",
  edad: 3,
  dueno: "Carlos",
  vacunada: true,
  raza: "Labrador"
}

console.log(perro.nombre) // "Rex"
console.log(perro.raza)   // "Labrador"

Si una interfaz extiende otra e intenta redefinir una propiedad con un tipo incompatible, TypeScript genera un error claro:

interface Base {
  id: number
}

// Error: Interface 'Derivada' incorrectly extends interface 'Base'.
// Types of property 'id' are incompatible.
// interface Derivada extends Base {
//   id: string
// }

Alias de tipo con intersección

Los alias de tipo usan el operador & para combinar tipos. Esto produce una intersección que requiere que el valor satisfaga todos los tipos combinados:

type Animal = {
  nombre: string
  edad: number
}

type ConDueno = {
  dueno: string
  vacunada: boolean
}

type ConRaza = {
  raza: string
}

type Perro = Animal & ConDueno & ConRaza

const perro: Perro = {
  nombre: "Luna",
  edad: 2,
  dueno: "Elena",
  vacunada: true,
  raza: "Golden Retriever"
}

console.log(perro.nombre) // "Luna"
console.log(perro.dueno)  // "Elena"

Diferencia en conflictos de tipos

La diferencia crítica aparece cuando hay propiedades con tipos incompatibles. Con extends, TypeScript genera un error explícito. Con intersecciones, el tipo resultante es never para esa propiedad, lo que puede causar errores confusos:

type ConNombreTexto = {
  nombre: string
}

type ConNombreNumero = {
  nombre: number
}

// La intersección no genera error, pero nombre es never
type Combinado = ConNombreTexto & ConNombreNumero

// Error al intentar asignar: Type 'string' is not assignable to type 'never'
// const obj: Combinado = { nombre: "test" }

Con interfaces, el error es inmediato y descriptivo:

interface ConNombreTextoI {
  nombre: string
}

// Error claro: Types of property 'nombre' are incompatible
// interface ConNombreNumeroI extends ConNombreTextoI {
//   nombre: number
// }

En general, extends produce errores más legibles que las intersecciones cuando hay conflictos de tipos.

Mapped types y propiedades computadas

Mapped types: solo con type

Los mapped types permiten transformar las propiedades de un tipo existente. Esta capacidad es exclusiva de los alias de tipo:

type Usuario = {
  nombre: string
  email: string
  edad: number
}

// Hacer todas las propiedades opcionales
type UsuarioOpcional = {
  [K in keyof Usuario]?: Usuario[K]
}

// Hacer todas las propiedades readonly
type UsuarioInmutable = {
  readonly [K in keyof Usuario]: Usuario[K]
}

// Transformar los tipos de todas las propiedades
type UsuarioSerializado = {
  [K in keyof Usuario]: string
}

const parcial: UsuarioOpcional = { nombre: "Ana" }
console.log(parcial.nombre) // "Ana"

const inmutable: UsuarioInmutable = { nombre: "Carlos", email: "c@mail.com", edad: 30 }
// inmutable.nombre = "otro" // Error: readonly

const serializado: UsuarioSerializado = { nombre: "Elena", email: "e@mail.com", edad: "28" }
console.log(serializado.edad) // "28" (string)

No es posible definir mapped types con interface:

// Error: A mapped type may not declare properties or methods
// interface UsuarioOpcionalI {
//   [K in keyof Usuario]?: Usuario[K]
// }

Propiedades computadas con template literals

Los alias de tipo también soportan claves computadas con template literal types:

type Evento = "click" | "hover" | "focus"

type Manejadores = {
  [E in Evento as `on${Capitalize<E>}`]: () => void
}

// Manejadores = { onClick: () => void; onHover: () => void; onFocus: () => void }
const manejadores: Manejadores = {
  onClick: () => console.log("click"),
  onHover: () => console.log("hover"),
  onFocus: () => console.log("focus")
}

manejadores.onClick() // "click"
manejadores.onFocus() // "focus"

Tipos condicionales: solo con type

Los tipos condicionales son otra herramienta exclusiva de los alias de tipo:

type EsString<T> = T extends string ? true : false

type A = EsString<string>   // true
type B = EsString<number>   // false

type Aplanar<T> = T extends Array<infer U> ? U : T

type C = Aplanar<string[]>  // string
type D = Aplanar<number>    // number

Cuando usar cada uno

Usa interface cuando

Las interfaces son la mejor opción para definir la forma de objetos y APIs publicas extensibles:

// Contratos de objetos
interface Repositorio<T> {
  buscarPorId(id: string): T | undefined
  buscarTodos(): T[]
  guardar(entidad: T): void
  eliminar(id: string): boolean
}

// APIs extensibles (declaration merging permite ampliar)
interface OpcionesPeticion {
  url: string
  método: "GET" | "POST" | "PUT" | "DELETE"
}

interface OpcionesPeticion {
  cabeceras?: Record<string, string>
  cuerpo?: unknown
  timeout?: number
}

const peticion: OpcionesPeticion = {
  url: "/api/datos",
  método: "GET",
  cabeceras: { "Accept": "application/json" },
  timeout: 5000
}

console.log(peticion.url)     // "/api/datos"
console.log(peticion.timeout) // 5000

Usa type cuando

Los alias de tipo son necesarios para uniones, tuplas, tipos primitivos con alias, mapped types y transformaciones:

// Uniones discriminadas
type Resultado =
  | { exito: true; datos: string }
  | { exito: false; error: string }

function procesarResultado(resultado: Resultado): string {
  if (resultado.exito) {
    return `Datos: ${resultado.datos}`
  }
  return `Error: ${resultado.error}`
}

// Tuplas con etiquetas
type RangoFecha = [inicio: Date, fin: Date]

// Tipos de función
type Validador<T> = (valor: T) => boolean

// Tipos extraidos de otros
type ClavesUsuario = keyof Usuario
type TipoNombre = Usuario["nombre"]

const validarPositivo: Validador<number> = (n) => n > 0
console.log(validarPositivo(5))  // true
console.log(validarPositivo(-1)) // false

Comparativa directa

// EXTENDS vs INTERSECCION
interface EmpleadoI extends Animal {
  empresa: string
}

type EmpleadoT = Animal & {
  empresa: string
}

// IMPLEMENTACION EN CLASES: ambos funcionan
interface Serializable {
  serializar(): string
}

type Deserializable = {
  deserializar(datos: string): void
}

class Documento implements Serializable {
  contenido: string = ""

  serializar(): string {
    return JSON.stringify({ contenido: this.contenido })
  }
}

// COMPATIBILIDAD ESTRUCTURAL: identica
const doc: Serializable = new Documento()
console.log(doc.serializar()) // '{"contenido":""}'

Convenciones de la comunidad

La mayoría de proyectos TypeScript siguen estas convenciones:

// Convenciones comunes:

// 1. Interface para formas de objetos y contratos publicos
interface UsuarioDTO {
  id: number
  nombre: string
  email: string
}

// 2. Type para uniones, intersecciones y transformaciones
type EstadoPedido = "pendiente" | "enviado" | "entregado" | "cancelado"
type Parcial<T> = { [K in keyof T]?: T[K] }

// 3. Type para funciones y alias de primitivos
type ManejadorError = (error: Error) => void
type UUID = string

// 4. Interface para APIs que terceros puedan extender
interface PluginOpciones {
  nombre: string
  versión: string
}

// 5. Consistencia: elegir uno para objetos y mantenerlo en el proyecto
// Muchos equipos eligen interface para objetos por defecto
// y type solo cuando interface no es suficiente

La clave es mantener la consistencia dentro de un proyecto. Si el equipo elige interfaces para objetos, usar interfaces para todos los objetos. Si elige tipos, mantener esa convención. Lo importante es no mezclar sin criterio.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender las diferencias entre type e interface en TypeScript. Saber cuando usar declaration merging exclusivo de interfaces. Comparar extends con intersecciones para composición de tipos. Aplicar mapped types y propiedades computadas solo disponibles con type.