Conditional types: tipos que toman decisiones
Los conditional types llevan la lógica condicional al nivel de los tipos. Con ellos puedes construir tipos que se adaptan según lo que reciben — la misma idea que un ternario, pero en el sistema de tipos.
Sintaxis — T extends U ? X : Y
Un conditional type evalúa una condición sobre un tipo y devuelve uno de dos tipos posibles. La sintaxis es idéntica al operador ternario de JavaScript, pero todo ocurre en tiempo de compilación.
// Forma básica: T extends U ? X : Y
// "Si T es asignable a U, el tipo es X; si no, es Y"
type EsString<T> = T extends string ? "sí" : "no";
type R1 = EsString<string>; // "sí"
type R2 = EsString<number>; // "no"
type R3 = EsString<"hola">; // "sí" — el literal "hola" extiende string
// Un uso muy frecuente: IsArray<T>
type EsArray<T> = T extends any[] ? true : false;
type A1 = EsArray<number[]>; // true
type A2 = EsArray<string>; // false
type A3 = EsArray<never[]>; // true
// Conditional types con restricciones genéricas
type Aplanar<T> = T extends any[] ? T[number] : T;
type F1 = Aplanar<string[]>; // string
type F2 = Aplanar<number[]>; // number
type F3 = Aplanar<boolean>; // boolean — no es array, devuelve T
function aplanar<T>(valor: T): Aplanar<T> {
if (Array.isArray(valor)) {
return valor[0] as Aplanar<T>;
}
return valor as Aplanar<T>;
}
console.log(aplanar([1, 2, 3])); // 1
console.log(aplanar("texto")); // "texto"1
textoinfer — extraer partes de un tipo
infer es una palabra clave especial dentro de conditional types que captura una parte del tipo que estás verificando y la pone disponible como variable de tipo. Permite diseccionar tipos complejos.
// Extraer el tipo de retorno de una función (así funciona ReturnType<T>)
type MiReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function obtenerUsuario(id: number) {
return { id, nombre: "Ana", activo: true };
}
type TipoRetorno = MiReturnType<typeof obtenerUsuario>;
// { id: number; nombre: string; activo: boolean }
// Extraer el tipo de los parámetros (así funciona Parameters<T>)
type MisParameters<T> = T extends (...args: infer P) => any ? P : never;
function crearElemento(tag: string, clase: string, id: number): HTMLElement {
return document.createElement(tag);
}
type Params = MisParameters<typeof crearElemento>;
// [tag: string, clase: string, id: number]
// Extraer el tipo del primer elemento de una tupla
type Primero<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type P1 = Primero<[string, number, boolean]>; // string
type P2 = Primero<[number, ...string[]]>; // number
// Extraer el tipo de una Promise (así funciona Awaited<T> en parte)
type Desenvuelto<T> = T extends Promise<infer V> ? V : T;
type D1 = Desenvuelto<Promise<string>>; // string
type D2 = Desenvuelto<Promise<number[]>>; // number[]
type D3 = Desenvuelto<boolean>; // boolean
// Extraer el tipo de los elementos de un array
type ElementoArray<T> = T extends (infer E)[] ? E : never;
type E1 = ElementoArray<string[]>; // string
type E2 = ElementoArray<number[][]>; // number[]
console.log("infer funciona en tiempo de compilación");infer funciona en tiempo de compilacióninfer solo es válido dentro de la cláusula extends de un conditional type. No puedes usarlo en posición de tipo directamente. Siempre aparece como T extends ... infer R ....
Distributive conditional types — se distribuyen sobre unions
Cuando aplicas un conditional type a un tipo genérico y le pasas una union, TypeScript lo distribuye automáticamente: aplica el conditional a cada miembro por separado y combina los resultados.
// Conditional type distribuido: T es genérico (no wrapping)
type Distribuido<T> = T extends string ? "string" : "otro";
// Con una union:
type R1 = Distribuido<string | number | boolean>;
// Aplica a cada miembro:
// string → "string"
// number → "otro"
// boolean → "otro"
// Resultado: "string" | "otro" | "otro" = "string" | "otro"
// Exclude<T, U> se implementa exactamente así:
type MiExclude<T, U> = T extends U ? never : T;
type SinStrings = MiExclude<string | number | boolean, string>;
// string → never (excluido)
// number → number
// boolean → boolean
// Resultado: number | boolean
// Extract<T, U> — lo contrario:
type MiExtract<T, U> = T extends U ? T : never;
type SoloStrings = MiExtract<string | number | boolean, string>;
// string → string
// number → never
// boolean → never
// Resultado: string
// Para EVITAR la distribución: envuelve en tupla
type NoDistribuido<T> = [T] extends [string] ? "string" : "otro";
type R2 = NoDistribuido<string | number>;
// [string | number] extends [string]? → No → "otro"
// No se distribuye — evalúa la union completa
console.log("Distribución automática en unions");Distribución automática en unionsUtility types implementados con conditional types
Muchos utility types de la stdlib de TypeScript son conditional types. Verlos implementados consolida la comprensión:
// Reimplementaciones de la stdlib — idénticas al original
// NonNullable<T>
type MiNonNullable<T> = T extends null | undefined ? never : T;
type Test1 = MiNonNullable<string | null | undefined>; // string
// ReturnType<T>
type MiReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
// Parameters<T>
type MiParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// ConstructorParameters<T> — parámetros del constructor
type MiConstructorParams<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never;
class Servicio {
constructor(public url: string, public timeout: number) {}
}
type ParamsServicio = MiConstructorParams<typeof Servicio>;
// [url: string, timeout: number]
// InstanceType<T> — tipo de la instancia de un constructor
type MiInstanceType<T extends new (...args: any) => any> =
T extends new (...args: any) => infer I ? I : never;
type InstanciaServicio = MiInstanceType<typeof Servicio>;
// Servicio
// Awaited<T> — recursivo para Promises anidadas
type MiAwaited<T> =
T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: any): any }
? F extends (value: infer V, ...args: any) => any
? MiAwaited<V>
: never
: T;
type A1 = MiAwaited<Promise<string>>; // string
type A2 = MiAwaited<Promise<Promise<number>>>; // numberCasos avanzados — tipos recursivos
Los conditional types pueden ser recursivos, lo que permite construir tipos que operan sobre estructuras anidadas. TypeScript 4.1+ soporta recursión con límite de profundidad.
// DeepReadonly<T> — hace readonly todas las propiedades, en profundidad
type DeepReadonly<T> =
T extends (infer E)[]
? ReadonlyArray<DeepReadonly<E>> // array → aplica recursión
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> } // objeto → aplica a cada prop
: T; // primitivo → tal cual
interface Config {
servidor: {
host: string;
puerto: number;
ssl: {
certificado: string;
expiresAt: Date;
};
};
caracteristicas: string[];
}
type ConfigInmutable = DeepReadonly<Config>;
const config: ConfigInmutable = {
servidor: {
host: "api.ejemplo.com",
puerto: 443,
ssl: { certificado: "cert.pem", expiresAt: new Date() },
},
caracteristicas: ["autenticacion", "cache"],
};
// config.servidor.puerto = 80; // ❌ Error — readonly en profundidad
// config.caracteristicas.push("x"); // ❌ Error — ReadonlyArray
// Flatten recursivo (profundidad fija con recursión)
type Flatten<T> =
T extends Array<infer E>
? Flatten<E>
: T;
type F1 = Flatten<number[][][]>; // number
type F2 = Flatten<string[]>; // string
type F3 = Flatten<Promise<string>>; // Promise<string> — no es array
console.log("Tipos recursivos resueltos en compilación");Tipos recursivos resueltos en compilaciónTypeScript tiene un límite de profundidad de recursión en tipos (alrededor de 100 niveles). Para estructuras muy profundas, puede que necesites estrategias alternativas. Siempre verifica que tu tipo recursivo converge.