La incorporación de los tipos condicionales (Conditional Types) en TypeScript 2.8 transformó el sistema de tipos en un lenguaje de programación funcional Turing-completo que se evalúa durante la compilación estática. Los tipos condicionales permiten expresar relaciones no lineales entre tipos de entrada y estructuras de datos resultantes, eligiendo una de dos ramas de computación a partir de una relación de subtipado.
La sintaxis de un tipo condicional reproduce el operador ternario de JavaScript:
type Result = T extends U ? TrueBranch : FalseBranch;Si el tipo T es un subtipo del tipo U (T <: U), se evalúa la rama TrueBranch; de lo contrario, se procesa la rama FalseBranch.
Dos mecanismos fundamentales multiplican el poder expresivo de los tipos condicionales:
infer: mecanismo de coincidencia de patrones (Pattern Matching) que permite declarar una variable de tipo local dentro de la cláusula extends para extraer un subtipo estructural anidado.La combinación de tipos condicionales e infer constituye el núcleo de todas las arquitecturas de metaprogramación modernas: desde utilidades de la librería estándar como ReturnType, Parameters y Awaited, hasta complejos analizadores de esquemas en tRPC y ORMs.
Al evaluar un tipo condicional, el compilador analiza la naturaleza del operando T. Si T es un parámetro de tipo desnudo (naked type parameter) y se suministra una unión A | B | C, la operación se expande distributivamente:
+-------------------------------------------------------------------------+
| DISTRIBUTIVIDAD DE TIPOS CONDICIONALES |
| (A | B) extends U ? TrueBranch : FalseBranch |
+-------------------------------------------------------------------------+
| | |
| Expandido por el compilador en una unión de ramas: |
| v |
| (A extends U ? TrueBranch : FalseBranch) |
| | (Union) |
| (B extends U ? TrueBranch : FalseBranch) |
+-------------------------------------------------------------------------+
| |
| DESACTIVACIÓN DE DISTRIBUTIVIDAD VÍA TUPLAS [T] |
| [A | B] extends [U] ? TrueBranch : FalseBranch |
| | |
| v |
| Evalúa la unión completa (A | B) como una única entidad |
+-------------------------------------------------------------------------+La palabra clave infer actúa como un extractor de patrones: el compilador reconcilia el tipo entrante con la forma de destino y vincula la posición desconocida a un nuevo identificador de tipo.
infer sin duplicar manualmente las interfaces de respuesta en el cliente.Promise<Promise<string[]>> en el tipo plano string[] al procesar eventos de red.type SafePayload<T> = T extends AppError ? never : T.Payload de creadores de acciones: type ExtractPayload<T> = T extends ActionCreator<infer P> ? P : never.El compilador verifica si T es un parámetro desnudo. Si detecta una Unión (A | B), inicia un ciclo distributivo sobre cada miembro.
La estructura de T se unifica frente al objetivo extends. Si contiene infer R, se captura el componente en la variable R.
Múltiples candidatos para R en posiciones de salida convergen en Unión (|); en posiciones de entrada convergen en Intersección (&).
Sustituye la R inferida en TrueBranch o conmuta a FalseBranch, realizando la normalización final del tipo.
never#En TypeScript, never representa matemáticamente una unión vacía ($0$ miembros). Debido a esto, la evaluación distributiva sobre never produce un comportamiento inesperado si no se aísla adecuadamente:
// ❌ IMPLEMENTACIÓN INGENUA Y ERRÓNEA:
export type IsNeverWrong<T> = T extends never ? true : false;
// ¿Por qué IsNeverWrong<never> devuelve never en vez de true?
// Como never es una unión VACÍA, ¡el ciclo distributivo se ejecuta 0 veces!
// Resultado: unión vacía de ramas => ¡never!
type Test1 = IsNeverWrong<never>; // never!
// ✅ IMPLEMENTACIÓN CORRECTA: desactivar distributividad mediante tuplas [T]
export type IsNever<T> = [T] extends [never] ? true : false;
type Test2 = IsNever<never>; // true!
type Test3 = IsNever<string>; // false!
type Test4 = IsNever<unknown>; // false!Desactivar la distributividad también resulta indispensable al diseñar un predicado para determinar si un tipo es una unión:
// Comprobar si un tipo es una unión:
// Truco: [T] desactiva la distributividad en la comparación externa,
// mientras que T desnudo en la expresión interna se distribuye sobre cada miembro.
export type IsUnion<T, U = T> = T extends any
? [U] extends [T]
? false
: true
: never;
type Case1 = IsUnion<string>; // false
type Case2 = IsUnion<string | number>; // true
type Case3 = IsUnion<"a" | "b" | "c">; // true
type Case4 = IsUnion<[string, number]>; // false (la tupla es monolítica)infer: Extracción de parámetros y retornos#La palabra clave infer posibilita la desestructuración de firmas de funciones:
// 1. Extraer el primer argumento de una función
export type FirstArgument<TFunction> = TFunction extends (
first: infer TFirst,
...rest: readonly unknown[]
) => unknown
? TFirst
: never;
// 2. Extraer el último elemento de una tupla o array
export type LastElement<TTuple extends readonly unknown[]> = TTuple extends readonly [
...unknown[],
infer TLast
]
? TLast
: never;
// 3. Extraer el valor resuelto de una función asíncrona
export type AsyncReturnType<TFunction extends (...args: readonly unknown[]) => unknown> =
TFunction extends (...args: readonly unknown[]) => Promise<infer TResult>
? TResult
: ReturnType<TFunction>;
// Demostración
async function fetchUserMetrics(userId: string) {
return { userId, score: 98.5, active: true };
}
type TargetFirst = FirstArgument<typeof fetchUserMetrics>; // string
type TargetReturn = AsyncReturnType<typeof fetchUserMetrics>; // { userId: string, score: number, active: boolean }
type TargetTuple = LastElement<[number, string, boolean]>; // booleanAwaited<T>#A partir de TypeScript 4.5, Awaited<T> está implementado como un tipo condicional recursivo profundo capaz de desenvolver cadenas de objetos PromiseLike a cualquier profundidad con detección de ciclos:
// Implementación arquitectónica de un desenvuelto recursivo de objetos Thenable
export type DeepAwaited<T> = T extends null | undefined
? T // Si se pasa null/undefined, se retorna tal cual
: T extends object & { then(onfulfilled: infer F, ...args: readonly unknown[]): unknown }
? F extends (value: infer V, ...args: readonly unknown[]) => unknown
? DeepAwaited<V> // Descenso recursivo en el valor resuelto
: never
: T; // Tipo escalar base si T no es una promesa
// Demostración de desenvuelto profundo
type Level0 = DeepAwaited<string>; // string
type Level1 = DeepAwaited<Promise<string>>; // string
type Level2 = DeepAwaited<Promise<Promise<number>>>; // number
type Level3 = DeepAwaited<Promise<Promise<Promise<boolean>>>>; // booleaninfer ante múltiples apariciones#Cuando la misma variable de tipo infer R aparece en varias posiciones del patrón, el compilador aplica las reglas de varianza:
|).&).// Posición contravariante: los parámetros de función convergen en una Intersección (&)
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type DisjointUnion = { role: "admin" } | { permissions: string[] };
// Transformación a nivel de tipos: ¡la unión se convierte en intersección!
type Intersected = UnionToIntersection<DisjointUnion>;
// Resultado: { role: "admin" } & { permissions: string[] }A continuación se muestra una utilidad de metaprogramación que extrae la metainformación de cualquier función suministrada (síncrona o asíncrona):
export type FunctionDescriptor<TFunction> = TFunction extends (
this: infer TThis,
...args: infer TArgs
) => infer TReturn
? {
thisContext: TThis;
parameters: TArgs;
parameterCount: TArgs extends { length: infer L } ? L : 0;
rawReturnType: TReturn;
isAsync: TReturn extends Promise<unknown> ? true : false;
resolvedReturnType: TReturn extends Promise<infer TResolved>
? TResolved
: TReturn;
}
: never;
// Demostración de uso
class PaymentService {
async processTransaction(this: PaymentService, orderId: string, amount: number) {
return { transactionId: "tx-42", status: "COMPLETED" as const };
}
}
export type PaymentMethodInfo = FunctionDescriptor<PaymentService["processTransaction"]>;
/*
PaymentMethodInfo se resuelve en tiempo de compilación a:
{
thisContext: PaymentService;
parameters: [orderId: string, amount: number];
parameterCount: 2;
rawReturnType: Promise<{ transactionId: string; status: "COMPLETED" }>;
isAsync: true;
resolvedReturnType: { transactionId: string; status: "COMPLETED" };
}
*/Dado que never representa una unión vacía, T extends never se ejecuta distributivamente 0 veces y retorna incondicionalmente never. Cualquier comprobación sobre never debe envolverse en tuplas: [T] extends [never].
// ❌ INCORRECTO: devuelve never al pasar never
type CheckNever<T> = T extends never ? "is-never" : "not-never";
type Result1 = CheckNever<never>; // ¡never!
// ✅ CORREGIDO: comprobación no distributiva mediante tuplas
type CheckNeverSafe<T> = [T] extends [never] ? "is-never" : "not-never";
type Result2 = CheckNeverSafe<never>; // "is-never"Si un tipo condicional envuelve un parámetro sin proteger en un array, la distributividad genera una unión de arrays (A[] | B[]) en lugar de un array de uniones ((A | B)[]), quebrando el manejo de colecciones.
// ❌ ERROR: transforma string | number en string[] | number[]
type ToArrayDistributive<T> = T extends unknown ? T[] : never;
type MixedList1 = ToArrayDistributive<string | number>; // string[] | number[]
// ✅ CORRECTO: el aislamiento con tuplas preserva la unión
type ToArrayMonolithic<T> = [T] extends [unknown] ? T[] : never;
type MixedList2 = ToArrayMonolithic<string | number>; // (string | number)[]La palabra clave infer está permitida únicamente dentro de la cláusula extends de un tipo condicional. Intentar emplearla en el cuerpo de interfaces, parámetros genéricos o posiciones directas de retorno provocará el error de sintaxis TS1338.
// ❌ ERROR DE SINTAXIS TS1338: 'infer' declarations are only permitted in the 'extends' clause
type BadExtract<T> = infer R;
// ✅ CORRECTO: infer se usa estrictamente dentro de la condición extends
type GoodExtract<T> = T extends infer R ? R : never;En implementaciones recursivas descuidadas de infer sobre funciones sin valor de retorno, void puede colapsar indebidamente a undefined o never, invalidando los contratos de manejadores de eventos.
El compilador evalúa el tipo T: si es una unión y T no está envuelto en una tupla, se activa la distribución.
La expresión condicional se duplica para cada miembro individual de la unión.
Para cada rama, se unifica la estructura frente al patrón, capturando las variables de tipo desconocidas.
Todas las ramas evaluadas se combinan de nuevo en una unión resultante normalizada.