Saltar al contenido
← Blog
Desarrollo30 de julio de 20267 min de lectura

TypeScript avanzado: 7 patrones que reducen bugs en producción

TypeScript es más que añadir tipos a JavaScript. Estos 7 patrones avanzados usan el sistema de tipos para prevenir bugs enteros: desde type predicates hasta discriminated unions con exhaustive checking.

MM

softw.engineer

Software Engineer · softw.engineer

TypeScript avanzado: 7 patrones que reducen bugs en producción

TypeScript avanzado: 7 patrones que reducen bugs en producción

Si llevas años con TypeScript y sigues usándolo como "JavaScript con tipos en funciones", estás dejando el 80% de su poder sobre la mesa. Estos son los patrones que separan un TypeScript funcional de uno que previene bugs antes de que existan.

1. satisfies operator: validación sin pérdida de información

El problema clásico: quieres que un objeto cumpla una interfaz, pero sin perder la información específica de cada propiedad.

// SIN satisfies: pierdes los literales
const config = {
  api: '/api/v2',
  timeout: 5000,
} as const

// TypeScript sabe: { readonly api: "/api/v2"; readonly timeout: 5000 }
// Pero NO valida que cumpla tu tipo Config
interface Config {
  api: string
  timeout: number
}

const config = {
  api: '/api/v2',
  timeout: 5000,
} satisfies Config
// ✓ Valida que cumple Config
// ✓ Y preserva los literales: { api: "/api/v2", timeout: 5000 }

satisfies te da ambas cosas: validación de que el objeto cumple el tipo, y preservación de la información específica. Antes tenías que elegir.

// Ahora puedes hacer esto:
type Method = config.api // "/api/v2" — no `string`

2. Type predicates: type guards reutilizables y seguros

El patrón unknown → tipo específico necesita guards. Los type predicates hacen que esos guards sean verificados por TypeScript.

// Type predicate: `x is Error`
function isError(x: unknown): x is Error {
  return x instanceof Error
}

// Uso:
try {
  riskyOperation()
} catch (e: unknown) {
  if (isError(e)) {
    console.error(e.message) // ✓ TypeScript sabe que e es Error
  } else {
    console.error('Unknown error:', e)
  }
}

El poder está en que TypeScript verifica que el predicate sea consistente. Si tu función dice x is Error pero internamente chequea algo diferente, no compila.

Patrón avanzado: guards para APIs externas.

interface User {
  id: string
  email: string
  name: string
}

function isUser(x: unknown): x is User {
  return (
    typeof x === 'object' &&
    x !== null &&
    'id' in x &&
    'email' in x &&
    'name' in x &&
    typeof (x as User).id === 'string' &&
    typeof (x as User).email === 'string' &&
    typeof (x as User).name === 'string'
  )
}

// API response → tipo seguro
const response = await fetch('/api/user')
const data: unknown = await response.json()

if (isUser(data)) {
  // data es User aquí, garantizado por TypeScript
  console.log(data.email)
}

3. Discriminated unions con exhaustive checking

Si no usas discriminated unions para modelar estados, estás perdiendo el feature más poderoso de TypeScript.

type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

function renderState<T>(state: FetchState<T>) {
  switch (state.status) {
    case 'idle':
      return null
    case 'loading':
      return <Spinner />
    case 'success':
      return <Data data={state.data} /> // TypeScript sabe que data existe
    case 'error':
      return <Error error={state.error} /> // TypeScript sabe que error existe
    default:
      // exhaustive check: si añadimos un nuevo estado sin handle,
      // TypeScript error en tiempo de compilación
      const _exhaustive: never = state
      return _exhaustive
  }
}

El never en el default es la magia. Si mañana añades { status: 'cached'; data: T } a la unión y olvidas handle el caso, TypeScript te grita en compilación. Tu código es literalmente imposible de romper silenciosamente.

4. Template literal types: strings validados por el compilador

type APIEndpoint = `/api/${string}`

function fetchAPI(endpoint: APIEndpoint): Promise<unknown> {
  return fetch(endpoint).then(r => r.json())
}

fetchAPI('/api/users')      // ✓ válido
fetchAPI('/api/v2/posts/42') // ✓ válido
fetchAPI('https://example.com') // ✗ TypeError

TypeScript valida que el string empiece con /api/. Tiny pero poderoso.

Patrón más útil: rutas tipadas.

type Route =
  | '/'
  | `/blog/${string}`
  | `/product/${string}`

function navigate(route: Route) {
  window.history.pushState({}, '', route)
}

navigate('/blog/my-post')   // ✓
navigate('/blog/')          // ✓
navigate('/random/path')    // ✗ error

5. Utility types condicionales: extraer tipos por valor

// Extrae solo las keys cuyos valores son de un tipo específico
type PickByValue<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K]
}

interface FormData {
  name: string
  email: string
  age: number
  newsletter: boolean
}

type StringFields = PickByValue<FormData, string>
// { name: string; email: string }

type NumberFields = PickByValue<FormData, number>
// { age: number }

Esto es oro para formularios dinámicos. Puedes tipar validaciones que solo aplican a campos de cierto tipo.

// Solo validar campos string
function validateStrings(data: StringFields): StringFields {
  // data solo tiene name y email, ambos strings
  return {
    name: data.name.trim(),
    email: data.email.trim().toLowerCase(),
  }
}

6. const type parameters: tipos sin widening

// Sin const: T se infiere como string[]
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}
const x = first(['a', 'b', 'c'])
// type de x: string | undefined

// Con const: T preserva los literales
function firstConst<const T>(arr: readonly T[]): T | undefined {
  return arr[0]
}
const y = firstConst(['a', 'b', 'c'])
// type de y: "a" | "b" | "c" | undefined

El const previene que TypeScript ensanche los tipos. Esto te permite hacer utilidades que preservan información de tipos sin que el usuario tenga que añadir as const.

7. Mapped types para APIs type-safe

// Crear un tipo "opcional" basado en otro tipo
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

interface User {
  id: string
  email: string
  name: string
  role: 'admin' | 'user'
}

// En un form de edición, el id no se edita
type EditableUser = PartialBy<User, 'id'>
// { id: string; email?: string; name?: string; role?: 'admin' | 'user' }

Esto evita el problema de tener que mantener interfaces paralelas a mano. Si User cambia, EditableUser se actualiza automáticamente.

El patrón que une todo: diseño de tipos como arquitectura

El verdadero poder de TypeScript no está en añadir tipos a JavaScript. Está en usar el sistema de tipos como herramienta de arquitectura.

// Estado inválido imposible de representar
type UserAccount =
  | { status: 'guest' }
  | { status: 'registered'; email: string; verified: boolean }
  | { status: 'premium'; email: string; verified: true; plan: 'monthly' | 'yearly' }

function canAccessPremium(user: UserAccount): boolean {
  if (user.status === 'premium') {
    return true // TypeScript garantiza que verified es true aquí
  }
  return false
}

// Esto no compila: un guest no tiene email
const guestEmail: string | undefined = (
  'email' in user ? user.email : undefined
)

Tu código no puede representar estados inválidos. El bug no existe porque el compilador no lo permite.

Esa es la diferencia entre TypeScript "funcional" y TypeScript como herramienta de diseño de software.

Cuándo no usar TypeScript avanzado

Type safety tiene un costo: complejidad cognitiva. Si tu equipo es nuevo en TypeScript, no empieces con discriminated unions con never. Empieza con tipos básicos.

Los patrones avanzados brillan en:

  • APIs públicas: librerías, SDKs, packages compartidos
  • Dominios complejos: lógica de negocio con muchos estados
  • Equipos grandes: donde el sistema de tipos reemplaza documentación
  • Código crítico: pagos, autenticación, permisos

No brillan en:

  • Scripts desechables: el overhead de tipos no se paga
  • Prototipos rápidos: prioriza velocidad
  • Equipos pequeños con buena comunicación: el overhead puede ser net negativo

La regla: el tipo debe prevenir bugs. Si el tipo solo hace el código más difícil de leer sin prevenir nada, elimínalo.


TypeScript es un lenguaje donde la diferencia entre un junior y un senior no es cuánto saben de JavaScript. Es cuánto saben del sistema de tipos como herramienta de diseño.

Si solo usas tipos para documentar funciones, estás en el 10% del camino. Los patrones que vimos hoy son el siguiente 40%. El resto es práctica.

#TypeScript#types#patterns#type-safety#frontend

Compartir

¿Qué te pareció esto?

Tu feedback nos ayuda a crear mejor contenido

¿Qué opinas?

Déjanos tu comentario o pregunta sobre este artículo.