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.

// 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
UsuarioIpuede asignarse a una variable de tipoUsuarioTy 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,
extendsproduce 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
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.