Frontend 6 jul 2026 · 7 min de lectura

Modelar el dominio con tipos: cuando el compilador escribe tus tests

Yohangel Ramos

Yohangel Ramos

Tech Lead · Senior Fullstack Developer

La mayoría del TypeScript que veo usa los tipos como una capa de autocompletado: un interface por aquí, un any por allá cuando molesta, y a correr. Funciona, pero desperdicia lo más valioso que ofrece el lenguaje. Si modelas bien el dominio, el compilador deja de ser un corrector ortográfico y pasa a ser el que impide que existan estados que no deberían existir. En los paneles y PWAs que construyo, un montón de bugs que antes cazaba con tests o en revisión ahora ni siquiera compilan. Este artículo es cómo llegué a usar el sistema de tipos como parte del diseño, no como decoración.

Los estados imposibles no deberían ser representables

El patrón que más me cambió la forma de escribir código es este: si un estado no puede ocurrir en tu dominio, haz que tampoco pueda escribirse en tu tipo. El ejemplo canónico es el clásico "cargando / cargado / error". La versión ingenua lo modela con campos opcionales sueltos:

// El tipo permite estados que no tienen sentido:
// loading true y data presente a la vez, o error con data.
type State = {
  loading: boolean;
  data?: Resultado;
  error?: Error;
};

Ese tipo admite { loading: true, data: algo, error: otraCosa }, un estado que jamás debería existir pero que el compilador acepta encantado. Cada componente que lo consume tiene que acordarse de comprobar las combinaciones a mano, y el día que a alguien se le olvida, aparece un bug. La unión discriminada cierra la puerta:

// Ahora cada variante lleva exactamente los datos que tiene,
// y ninguna combinación imposible es representable.
type State =
  | { status: 'loading' }
  | { status: 'success'; data: Resultado }
  | { status: 'error'; error: Error };

Con esta versión, acceder a data sin comprobar antes que status es 'success' no compila. El compilador te obliga a tratar cada caso, y el estado "cargando con datos y error a la vez" simplemente no puede escribirse. No he añadido un solo test y he eliminado una familia entera de bugs.

Tipos marcados: no todos los strings son iguales

Un string puede ser un id de usuario, un email o un token, y para TypeScript son intercambiables. Eso significa que pasar un id donde iba un email compila sin queja, y ese error se descubre en tiempo de ejecución o nunca. Los tipos marcados (branded types) le ponen una etiqueta al tipo para que el compilador distinga cosas que estructuralmente son iguales pero conceptualmente no.

type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };

function enviarInvitacion(to: Email) { /* ... */ }

const id = 'usr_123' as UserId;
enviarInvitacion(id); // ❌ no compila: UserId no es Email

El truco es que el __brand no existe en tiempo de ejecución —es puro tipo, cero coste— pero fuerza a que un UserId y un Email no se confundan. Combinado con validación en la frontera (una función que valida un string y devuelve un Email marcado), consigues que a partir de ese punto el sistema de tipos garantice que lo que circula ya está validado. La validación deja de ser algo que "espero que alguien haya hecho antes".

💡 Cuando dudes entre validar en tiempo de ejecución o confiar en un tipo, haz ambas cosas una vez en la frontera: valida el dato de entrada y devuélvelo marcado. A partir de ahí el compilador propaga esa garantía gratis por todo el código, sin una sola comprobación más.

Dejar que la inferencia trabaje en lugar de anotar todo

Un error frecuente es anotar tipos en todas partes por costumbre, incluso donde TypeScript los infiere mejor que tú. Anotar de más no es más seguro: acopla tu código a nombres de tipo que luego cuesta cambiar, y a veces esconde con un tipo ancho lo que la inferencia habría dejado preciso. Prefiero anotar las fronteras —las firmas públicas de funciones, los contratos entre módulos— y dejar que la inferencia haga el trabajo dentro.

// `as const` hace que el tipo inferido sea exacto,
// no un `string[]` genérico.
const TAGS = ['IA', 'AWS', 'Frontend'] as const;
type Tag = (typeof TAGS)[number]; // 'IA' | 'AWS' | 'Frontend'

Ese patrón me deja tener una única fuente de verdad —el array TAGS— y derivar el tipo de ella. Si mañana añado un tag al array, el tipo se actualiza solo y todos los sitios que hacían un switch exhaustivo sobre Tag empiezan a dar error de compilación hasta que trato el caso nuevo. El dato y el tipo no se pueden desincronizar porque uno se deriva del otro.

El coste, porque siempre lo hay

Modelar el dominio con tipos ricos no es gratis. Las uniones discriminadas obligan a escribir más ramas explícitas, los tipos marcados añaden ceremonia en las fronteras, y hay un punto en el que insistir en expresar una invariante en el sistema de tipos produce firmas que nadie quiere leer. Elegí este estilo asumiendo ese coste porque en un panel que mantengo durante años el compilador es el único revisor que nunca se cansa ni se despista, y cada invariante que consigo expresar como un tipo es un test que no tengo que escribir ni mantener. Pero sé cuándo parar: si un tipo se vuelve más difícil de entender que el bug que previene, ahí una comprobación en tiempo de ejecución y un comentario honesto ganan.

Dónde pongo el esfuerzo hoy

Mi regla práctica es invertir en tipos allí donde el dominio tiene reglas que de verdad importan —qué estados son válidos, qué datos están validados, qué valores son intercambiables y cuáles no— y no gastar esfuerzo en tipar hasta el último detalle interno que la inferencia ya cubre. Usado así, TypeScript deja de ser una molestia que te pide anotaciones y se convierte en una herramienta de diseño: escribes el modelo una vez, y el compilador se encarga de que nadie, tú incluido dentro de seis meses, pueda usarlo mal sin enterarse.

Yohangel Ramos

Escrito por Yohangel Ramos

Senior Fullstack Developer y Tech Lead. Construyo con React, Next.js, Nest.js y AWS — y escribo sobre lo que aprendo en el camino.

Hablemos →

Sigue leyendo

IA

Embeddings más baratos: recortar dimensiones y cuantizar sin perder recall

IA

Qué va a pasar con el desarrollo de software: predicciones con fechas (julio 2026)