Patrones de tipado avanzado en TypeScript

Avanzado
TypeScript
TypeScript
Actualizado: 18/04/2026

Builder pattern con tipado progresivo

El patrón Builder permite construir objetos complejos paso a paso. En TypeScript, se puede tipar de forma que el compilador rastree que propiedades se han configurado y solo permita llamar a build() cuando el objeto está completo.

Maquina de estados con tipos discriminados

interface ConfiguracionHTTP {
  url: string
  método: "GET" | "POST" | "PUT" | "DELETE"
  headers: Record<string, string>
  timeout: number
}

class HTTPBuilder<T extends Partial<ConfiguracionHTTP> = {}> {
  private config: Partial<ConfiguracionHTTP> = {}

  url(url: string): HTTPBuilder<T & { url: string }> {
    this.config.url = url
    return this as any
  }

  método(método: ConfiguracionHTTP["método"]): HTTPBuilder<T & { método: string }> {
    this.config.método = método
    return this as any
  }

  header(nombre: string, valor: string): HTTPBuilder<T & { headers: Record<string, string> }> {
    this.config.headers = { ...this.config.headers, [nombre]: valor }
    return this as any
  }

  timeout(ms: number): HTTPBuilder<T & { timeout: number }> {
    this.config.timeout = ms
    return this as any
  }

  build(
    this: HTTPBuilder<{ url: string, método: string }>
  ): ConfiguracionHTTP {
    return {
      url: (this as any).config.url,
      método: (this as any).config.método,
      headers: (this as any).config.headers ?? {},
      timeout: (this as any).config.timeout ?? 5000
    }
  }
}

// Correcto: url y método están configurados
const peticion = new HTTPBuilder()
  .url("https://api.ejemplo.com/datos")
  .método("GET")
  .header("Authorization", "Bearer token123")
  .timeout(3000)
  .build()

console.log(peticion)

// Error de compilación: falta método()
// new HTTPBuilder().url("https://api.ejemplo.com").build()

El patrón Builder tipado garantiza en tiempo de compilación que los campos obligatorios están configurados antes de construir el objeto. Los campos opcionales pueden omitirse sin afectar la compilación.

Builder con validación de orden

En algunos casos, el orden de las operaciones importa. Se puede modelar esto con tipos que solo exponen ciertos métodos en cada etapa:

interface PipelineInicio {
  origen(tabla: string): PipelineFiltro
}

interface PipelineFiltro {
  filtrar(condición: string): PipelineFiltro
  ordenar(campo: string, dirección: "asc" | "desc"): PipelineOrden
  ejecutar(): string
}

interface PipelineOrden {
  limitar(cantidad: number): PipelineFinal
  ejecutar(): string
}

interface PipelineFinal {
  ejecutar(): string
}

class QueryBuilder implements PipelineInicio, PipelineFiltro, PipelineOrden, PipelineFinal {
  private partes: string[] = []

  origen(tabla: string): PipelineFiltro {
    this.partes.push(`SELECT * FROM ${tabla}`)
    return this
  }

  filtrar(condición: string): PipelineFiltro {
    this.partes.push(`WHERE ${condición}`)
    return this
  }

  ordenar(campo: string, dirección: "asc" | "desc"): PipelineOrden {
    this.partes.push(`ORDER BY ${campo} ${dirección.toUpperCase()}`)
    return this
  }

  limitar(cantidad: number): PipelineFinal {
    this.partes.push(`LIMIT ${cantidad}`)
    return this
  }

  ejecutar(): string {
    return this.partes.join(" ")
  }
}

function crearQuery(): PipelineInicio {
  return new QueryBuilder()
}

const consulta = crearQuery()
  .origen("usuarios")
  .filtrar("activo = true")
  .ordenar("nombre", "asc")
  .limitar(10)
  .ejecutar()

console.log(consulta)
// SELECT * FROM usuarios WHERE activo = true ORDER BY nombre ASC LIMIT 10

Sistemas de eventos tipados

Un sistema de eventos tipado garantiza que los manejadores reciben exactamente el tipo de dato correspondiente a cada evento:

interface MapaEventos {
  "usuario:login": { userId: string, timestamp: number }
  "usuario:logout": { userId: string }
  "producto:creado": { productoId: string, nombre: string, precio: number }
  "producto:eliminado": { productoId: string }
  "error": { código: number, mensaje: string }
}

type NombreEvento = keyof MapaEventos

class EventBus {
  private listeners = new Map<string, Set<Function>>()

  on<E extends NombreEvento>(
    evento: E,
    callback: (datos: MapaEventos[E]) => void
  ): () => void {
    if (!this.listeners.has(evento)) {
      this.listeners.set(evento, new Set())
    }
    this.listeners.get(evento)!.add(callback)

    // Devuelve función para cancelar suscripcion
    return () => {
      this.listeners.get(evento)?.delete(callback)
    }
  }

  emit<E extends NombreEvento>(evento: E, datos: MapaEventos[E]): void {
    const callbacks = this.listeners.get(evento)
    if (callbacks) {
      for (const cb of callbacks) {
        cb(datos)
      }
    }
  }
}

const bus = new EventBus()

// TypeScript infiere el tipo del parámetro automáticamente
bus.on("usuario:login", (datos) => {
  console.log(`Login: ${datos.userId} a las ${datos.timestamp}`)
})

bus.on("producto:creado", (datos) => {
  console.log(`Nuevo producto: ${datos.nombre} - ${datos.precio} EUR`)
})

const cancelar = bus.on("error", (datos) => {
  console.error(`Error ${datos.código}: ${datos.mensaje}`)
})

bus.emit("usuario:login", { userId: "usr_001", timestamp: Date.now() })
bus.emit("producto:creado", { productoId: "prod_01", nombre: "Monitor", precio: 299 })

// Error de compilación: faltan propiedades obligatorias
// bus.emit("producto:creado", { productoId: "prod_01" })

// Error de compilación: evento inexistente
// bus.emit("evento:falso", {})

cancelar() // Cancela la suscripcion al evento "error"

Sistema de eventos con once

class EventBusExtendido extends EventBus {
  once<E extends NombreEvento>(
    evento: E,
    callback: (datos: MapaEventos[E]) => void
  ): void {
    const cancelar = this.on(evento, (datos) => {
      cancelar()
      callback(datos)
    })
  }

  esperarEvento<E extends NombreEvento>(
    evento: E
  ): Promise<MapaEventos[E]> {
    return new Promise((resolve) => {
      this.once(evento, resolve)
    })
  }
}

const busExtendido = new EventBusExtendido()

// Uso con async/await
async function esperarLogin() {
  const datos = await busExtendido.esperarEvento("usuario:login")
  console.log(`Usuario ${datos.userId} ha iniciado sesion`)
}

Máquinas de estado con discriminated unions

Las máquinas de estado modelan sistemas que transicionan entre estados definidos. Las discriminated unions garantizan que cada estado tenga exactamente los datos que le corresponden:

type EstadoPedido =
  | { estado: "borrador", items: string[] }
  | { estado: "confirmado", items: string[], total: number, fechaConfirmacion: Date }
  | { estado: "pagado", items: string[], total: number, transaccionId: string }
  | { estado: "enviado", items: string[], total: number, codigoSeguimiento: string }
  | { estado: "entregado", items: string[], total: number, fechaEntrega: Date }
  | { estado: "cancelado", motivo: string }

function confirmarPedido(pedido: EstadoPedido & { estado: "borrador" }): EstadoPedido {
  if (pedido.items.length === 0) {
    return { estado: "cancelado", motivo: "Pedido sin items" }
  }

  const total = pedido.items.length * 10 // Precio simulado
  return {
    estado: "confirmado",
    items: pedido.items,
    total,
    fechaConfirmacion: new Date()
  }
}

function pagarPedido(
  pedido: EstadoPedido & { estado: "confirmado" },
  transaccionId: string
): EstadoPedido {
  return {
    estado: "pagado",
    items: pedido.items,
    total: pedido.total,
    transaccionId
  }
}

function enviarPedido(
  pedido: EstadoPedido & { estado: "pagado" },
  codigoSeguimiento: string
): EstadoPedido {
  return {
    estado: "enviado",
    items: pedido.items,
    total: pedido.total,
    codigoSeguimiento
  }
}

function describirPedido(pedido: EstadoPedido): string {
  switch (pedido.estado) {
    case "borrador":
      return `Borrador con ${pedido.items.length} items`
    case "confirmado":
      return `Confirmado: ${pedido.total} EUR el ${pedido.fechaConfirmacion.toLocaleDateString()}`
    case "pagado":
      return `Pagado: transaccion ${pedido.transaccionId}`
    case "enviado":
      return `Enviado: seguimiento ${pedido.codigoSeguimiento}`
    case "entregado":
      return `Entregado el ${pedido.fechaEntrega.toLocaleDateString()}`
    case "cancelado":
      return `Cancelado: ${pedido.motivo}`
    default:
      const _exhaustivo: never = pedido
      throw new Error(`Estado no manejado`)
  }
}

let pedido: EstadoPedido = { estado: "borrador", items: ["Laptop", "Raton"] }
console.log(describirPedido(pedido))

if (pedido.estado === "borrador") {
  pedido = confirmarPedido(pedido)
  console.log(describirPedido(pedido))
}

if (pedido.estado === "confirmado") {
  pedido = pagarPedido(pedido, "txn_abc123")
  console.log(describirPedido(pedido))
}

if (pedido.estado === "pagado") {
  pedido = enviarPedido(pedido, "TRACK_789")
  console.log(describirPedido(pedido))
}

Transiciones válidas como tipo

Se puede modelar que transiciones de estado son válidas para evitar saltos ilegales:

type TransicionesValidas = {
  borrador: "confirmado" | "cancelado"
  confirmado: "pagado" | "cancelado"
  pagado: "enviado"
  enviado: "entregado"
  entregado: never
  cancelado: never
}

type EstadoNombre = keyof TransicionesValidas

function puedeTransicionar(
  desde: EstadoNombre,
  hacia: EstadoNombre
): boolean {
  const transiciones: Record<EstadoNombre, readonly EstadoNombre[]> = {
    borrador: ["confirmado", "cancelado"],
    confirmado: ["pagado", "cancelado"],
    pagado: ["enviado"],
    enviado: ["entregado"],
    entregado: [],
    cancelado: []
  }

  return transiciones[desde].includes(hacia)
}

console.log(puedeTransicionar("borrador", "confirmado"))  // true
console.log(puedeTransicionar("borrador", "enviado"))      // false
console.log(puedeTransicionar("entregado", "borrador"))    // false

Respuestas de API tipadas

El tipado avanzado permite modelar respuestas de API donde el tipo de datos depende del endpoint o del código de estado:

interface MapaEndpoints {
  "/usuarios": { id: string, nombre: string, email: string }[]
  "/usuarios/:id": { id: string, nombre: string, email: string }
  "/productos": { id: string, nombre: string, precio: number }[]
  "/productos/:id": { id: string, nombre: string, precio: number }
}

type RespuestaAPI<T> =
  | { estado: "exito", datos: T, código: 200 }
  | { estado: "no_encontrado", mensaje: string, código: 404 }
  | { estado: "error_servidor", mensaje: string, código: 500 }
  | { estado: "no_autorizado", mensaje: string, código: 401 }

async function fetchAPI<E extends keyof MapaEndpoints>(
  endpoint: E
): Promise<RespuestaAPI<MapaEndpoints[E]>> {
  try {
    const response = await fetch(endpoint)

    if (response.ok) {
      const datos = await response.json() as MapaEndpoints[E]
      return { estado: "exito", datos, código: 200 }
    }

    if (response.status === 404) {
      return { estado: "no_encontrado", mensaje: "Recurso no encontrado", código: 404 }
    }

    if (response.status === 401) {
      return { estado: "no_autorizado", mensaje: "Autenticacion requerida", código: 401 }
    }

    return { estado: "error_servidor", mensaje: "Error interno", código: 500 }
  } catch {
    return { estado: "error_servidor", mensaje: "Error de conexión", código: 500 }
  }
}

async function ejemplo() {
  const respuesta = await fetchAPI("/usuarios")

  switch (respuesta.estado) {
    case "exito":
      // TypeScript sabe que datos es el array de usuarios
      for (const usuario of respuesta.datos) {
        console.log(`${usuario.nombre}: ${usuario.email}`)
      }
      break
    case "no_encontrado":
      console.log(respuesta.mensaje)
      break
    case "no_autorizado":
      console.log("Redirigiendo a login...")
      break
    case "error_servidor":
      console.error(respuesta.mensaje)
      break
  }
}

Readonly profundo y DeepFreeze

El patrón DeepReadonly convierte recursivamente todas las propiedades de un tipo en readonly, incluyendo objetos anidados y arrays:

type DeepReadonly<T> = T extends (infer U)[]
  ? readonly DeepReadonly<U>[]
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T

interface Configuración {
  servidor: {
    host: string
    puerto: number
    ssl: {
      activo: boolean
      certificado: string
    }
  }
  base_datos: {
    url: string
    opciones: string[]
  }
}

function congelarProfundo<T extends object>(obj: T): DeepReadonly<T> {
  for (const clave of Object.keys(obj)) {
    const valor = (obj as any)[clave]
    if (typeof valor === "object" && valor !== null) {
      congelarProfundo(valor)
    }
  }
  return Object.freeze(obj) as DeepReadonly<T>
}

const config = congelarProfundo({
  servidor: {
    host: "localhost",
    puerto: 3000,
    ssl: {
      activo: true,
      certificado: "/ruta/cert.pem"
    }
  },
  base_datos: {
    url: "postgres://localhost/app",
    opciones: ["sslmode=require"]
  }
})

console.log(config.servidor.host) // localhost

// Todos estos producen error de compilación:
// config.servidor.host = "otro"
// config.servidor.ssl.activo = false
// config.base_datos.opciones.push("nueva")

DeepPartial para actualizaciones parciales

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T

function fusionarProfundo<T extends object>(
  base: T,
  actualizacion: DeepPartial<T>
): T {
  const resultado = { ...base }

  for (const clave in actualizacion) {
    const valor = actualizacion[clave]
    if (
      typeof valor === "object" &&
      valor !== null &&
      !Array.isArray(valor) &&
      typeof (base as any)[clave] === "object"
    ) {
      (resultado as any)[clave] = fusionarProfundo(
        (base as any)[clave],
        valor as any
      )
    } else if (valor !== undefined) {
      (resultado as any)[clave] = valor
    }
  }

  return resultado
}

const configBase: Configuración = {
  servidor: {
    host: "localhost",
    puerto: 3000,
    ssl: { activo: false, certificado: "" }
  },
  base_datos: {
    url: "postgres://localhost/dev",
    opciones: []
  }
}

const configProduccion = fusionarProfundo(configBase, {
  servidor: {
    host: "producción.ejemplo.com",
    ssl: { activo: true, certificado: "/ruta/cert.pem" }
  },
  base_datos: {
    url: "postgres://db.ejemplo.com/prod"
  }
})

console.log(configProduccion.servidor.host) // producción.ejemplo.com
console.log(configProduccion.servidor.puerto) // 3000 (preservado de la base)
console.log(configProduccion.servidor.ssl.activo) // true

Estos patrones de tipado avanzado explotan las capacidades del sistema de tipos de TypeScript para trasladar verificaciones del tiempo de ejecución al tiempo de compilación. El Builder tipado, los eventos genéricos, las máquinas de estado con uniones discriminadas y los tipos recursivos como DeepReadonly permiten modelar dominios complejos con garantias que el compilador verifica automáticamente.

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

Implementar el patrón Builder con tipado progresivo y method chaining. Disenar sistemas de eventos tipados con genéricos. Modelar máquinas de estado con discriminated unions. Aplicar exhaustive switch con never para garantizar cobertura completa.