Cómo utilizar tipos condicionales en TypeScript para mejorar la robustez
¿Quieres que tus tipos de TypeScript hagan el trabajo sucio por ti —y de verdad— en vez de darte una falsa sensación de seguridad?
Tiempo estimado de lectura: 4 min
- Los tipos condicionales (T extends U ? X : Y) se evalúan en tiempo de compilación y permiten metaprogramación robusta.
- infer captura partes de tipos para extraer retornos, parámetros y elementos internos sin runtime.
- Patrones clave: Unwrap para Promises, evitar distribución con tuplas, manipular tuples con Head/Tail, y extraer retornos async.
- Precauciones: complejidad excesiva, deuda técnica y necesidad de validación runtime para datos externos.
Bien. Aquí tienes una guía práctica que no promete magia, pero sí te salva de la gimnasia de tipos inútil y de bugs que aparecen a las 2AM.
Resumen rápido (lectores con prisa)
Los condicionales de tipos son ternarios que TypeScript evalúa en tiempo de compilación. infer permite extraer partes de un tipo coincidente. Úsalos para utilidades tipo-level (Unwrap, AsyncReturn, Head/Tail) y evita su abuso: documenta, limita y combina con validación runtime cuando el input viene de la red.
Introducción
Primero, lo obvio: el condicional de tipos es un ternario que vive en el compilador. Sintaxis: T extends U ? X : Y. Se evalúa en tiempo de compilación. No en runtime. Eso lo convierte en una espada afilada: poderosa, pero cortante.
Condicionales de tipos
Sintaxis
Sintaxis: T extends U ? X : Y. Se evalúa en tiempo de compilación.
Ejemplo mínimo
type IsString<T> = T extends string ? true : false;
type A = IsString<'hola'>; // true
type B = IsString<42>; // false
Sencillo. Útil para construir utilidades.
infer
Qué es
infer captura piezas del tipo que estás inspeccionando. Es la manera de decirle a TS: “si esto encaja, arráncame esto otro”.
Ejemplo clásico — extraer el tipo de retorno
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => { id: number };
type R = MyReturn<Fn>; // { id: number }
Útil. Limpio. Poderoso.
Patrones
Patrón 1 — Unwrap de Promises (recursivo)
Cuando trabajas con APIs, las promesas anidadas son una plaga. Esto te limpia el resultado:
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type X = Unwrap<Promise<Promise<{ ok: true }>>>; // { ok: true }
Patrón 2 — Tipos distributivos y el problema que nadie lee
Si T es una unión, el condicional se aplica a cada miembro (distribuye). A veces quieres eso. A veces no.
Distribución:
type ExcludeFunc<T> = T extends Function ? never : T;
type U = ExcludeFunc<string | (() => void)>; // string
Evitar distribución
Envuélvelo en una tupla.
type NoDist<T> = [T] extends [Function] ? never : T;
Memorízalo. Te salvará de errores raros.
Patrón 3 — Extraer elementos de tuples/arrays
infer también domina tuplas. Necesitas sacar head/tail o el último elemento:
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;
Esto abre puertas para manipulaciones tipo-level sin hacer trampas.
Patrón 4 — AsyncReturnType para funciones async
type AsyncReturn<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;
Ideal para tipos de efectos y sagas.
Casos reales donde esto brilla
- Clientes HTTP tipados por endpoint (súper seguro, sin
asadivinatorio). - Librerías que exponen utilidades genéricas robustas.
- Metaprogramación de APIs internas (mapea rutas a tipos de response).
- Sistemas de validación y mapeo que dependen de signatures de funciones.
Ejemplo rápido: tipos por endpoint
interface Endpoints {
'/users': { id: string, name: string }[];
'/status': { ok: boolean };
}
function fetchApi<T extends keyof Endpoints>(url: T): Promise<Endpoints[T]> {
return fetch(url).then(r => r.json());
}
No más any. No más guessing.
Pitfalls — Lo que te rompe la vida si no tienes cuidado
- Complexity blowup: tipos gigantes ralentizan el compilador. TS puede confundirse y tirar errores crípticos.
- Abuso: si tu equipo no entiende, se convierte en deuda técnica invisible.
- Debug hard: errores en tipos a veces son opacos; documenta y divide tipos largos.
- Runtime vs compile-time: los tipos no validan data externa. Para eso, runtime schemas (Zod) son tus amigos.
Buenas prácticas — rápido y aplicable
- Usa tipos condicionales en librerías y núcleos infra. No en cada componente.
- Nombra aliases:
type AsyncReturn<T> = ...— no pongas 10 ternarios directos. - Comenta. Sí, los tipos necesitan comentarios.
- Mantén límites: si un tipo tiene más de 30–40 líneas, replantea.
- Combina con validación runtime donde el input viene de la red.
- Testea tipos con
tsdoexpect-typepara evitar regresiones.
Trucos avanzados en dos líneas
- Forzar no-distribución:
[T] extends [U] ? X : Y. - Obtener parámetros de una función:
type Params<T> = T extends (...a: infer A) => any ? A : never; - Extraer propiedades no-función: usa mapped types con condicionales para filtrar claves.
Metáfora rápida para que no lo olvides
Los tipos condicionales son el sistema inmunitario del código. Te protegen si los pones en el lugar correcto. Si los usas por todos lados sin control, te provocan una reacción autoinmune: complejidad que te derriba.
¿Quieres algo práctico que puedas pegar ya?
Puedo mandarte:
- Un cheat-sheet con 25 utilidades (
Unwrap,AsyncReturn,Head,Tail,PickByValue, etc.). - Un pequeño repo con ejemplos y tests de
tsd. - Un snippet para un cliente HTTP tipado por endpoints + validación Zod integrada.
Responde “Envíame el cheat-sheet” y te lo paso listo para copiar. No es teoría. Es la diferencia entre escribir tipos y dejar que los tipos te salven. Esto no acaba aquí.
FAQ
- ¿Qué es un condicional de tipos en TypeScript?
- ¿Qué hace
infer? - ¿Cómo evito que un condicional distribuya sobre una unión?
- ¿Los tipos garantizan que los datos externos sean válidos?
- ¿Cuándo debo usar estos patrones vs validación runtime?
- ¿Qué herramientas recomiendas para testear tipos?
¿Qué es un condicional de tipos en TypeScript?
Es una expresión tipo-level con la forma T extends U ? X : Y que el compilador evalúa en tiempo de compilación para producir un tipo según la condición.
¿Qué hace infer?
infer permite capturar una parte del tipo que coincide en un condicional, por ejemplo para extraer el tipo de retorno de una función o el inner type de una Promise.
¿Cómo evito que un condicional distribuya sobre una unión?
Envuelve el tipo en una tupla: [T] extends [U] ? X : Y. Eso evita la distribución sobre cada miembro de la unión.
¿Los tipos garantizan que los datos externos sean válidos?
No. Los tipos existen en tiempo de compilación. Para datos externos debes usar validación runtime (por ejemplo Zod) y combinarla con tipos TypeScript.
¿Cuándo debo usar estos patrones vs validación runtime?
Usa tipos condicionales y infer para modeling y seguridad interna del código. Añade validación runtime cuando aceptas datos de la red o de usuarios.
¿Qué herramientas recomiendas para testear tipos?
Testea tipos con frameworks como tsd o utilidades como expect-type para evitar regresiones en tipos complejos.
