Un conditional type elige entre dos tipos con una comprobación que se lee como el operador ternario de JavaScript: T extends U ? X : Y. Si T se puede asignar a U, el resultado es X; si no, es Y.
Aquí extends significa «se puede asignar a», la misma relación que usa el compilador cuando asignas un valor a una variable. Los conditional types solo existen en compilación; se borran del JavaScript de salida.
La sintaxis
type Result = CheckedType extends TestType ? TrueType : FalseType;
Los conditional types se vuelven útiles con genéricos, donde el tipo comprobado es un parámetro de tipo que recibe un tipo concreto más tarde. En la rama true, TypeScript sabe que el tipo comprobado cumple la condición, así que T["message"] es válido arriba aunque T a secas no tenga propiedad message.
También se anidan, como los ternarios encadenados:
El as TypeName<T> del cuerpo de la función no es opcional, como muestra la siguiente sección.
Los tipos de retorno condicionales necesitan una aserción
Una función cuyo tipo de retorno es un conditional type sobre su propio parámetro de tipo no puede devolver directamente ninguna de las dos ramas. TypeScript no reduce T dentro del cuerpo, así que no sabe qué rama se aplica:
index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.
Hay dos soluciones habituales: sobrecargas, que declaran cada par de entrada y salida y comprueban con precisión a quien llama, o una aserción en la implementación.
Las sobrecargas se explican en la página de sobrecarga de funciones. Con una aserción, el compilador confía en ti: no detectaría una rama equivocada en el cuerpo.
Distributive conditional types
Cuando el tipo comprobado es un parámetro de tipo sin envolver y recibe una unión, la condición se ejecuta una vez por miembro y los resultados se juntan en una unión nueva:
La distribución es lo que hace funcionar Exclude y Extract. Exclude<T, U> se define como T extends U ? never : T: cada miembro que encaja con U pasa a ser never, y never desaparece de una unión. Así, Exclude<"a" | "b" | "c", "a"> es "b" | "c".
La misma regla trae dos sorpresas. boolean es la unión true | false, así que ToArray<boolean> es false[] | true[], no boolean[]. Y never es la unión vacía, así que un distributive conditional type que recibe never devuelve never sin comprobar nada:
type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true
Extraer tipos con infer
infer declara una variable de tipo nueva dentro de la cláusula extends. Si la comparación encaja, TypeScript rellena esa variable a partir del tipo comprobado, y puedes usarla en la rama true:
Lee T extends Promise<infer V> ? V : T como «si T es una promesa de algo, llama a ese algo V y devuélvelo; si no, devuelve T sin cambios». infer solo se permite en la cláusula extends de un conditional type.
Una variable infer puede llevar su propia restricción con extends. Entonces la comparación solo encaja si el tipo inferido la cumple:
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string
Escribir tu propio ReturnType
El ReturnType integrado es un conditional type de una línea con infer. Escribirlo tú es el ejercicio clásico con el que se entienden las dos ideas:
typeof makeUser convierte el valor función en su tipo; después, el conditional type lo compara con «cualquier función» y captura el tipo de retorno como R. La versión de la librería estándar difiere en dos detalles: su parámetro está restringido a tipos función (T extends (...args: any) => any), así que ReturnType<string> es un error de compilación en lugar de never, y su rama falsa es any. La página de ReturnType también trata Parameters, InstanceType y Awaited.
Conditional types recursivos
Un conditional type puede referirse a sí mismo, lo que le permite desenvolver cualquier nivel de anidamiento:
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
El Awaited<T> integrado funciona así, desenvolviendo Promise<Promise<T>> hasta T. En la práctica, mantén la recursión poco profunda: una recursión muy profunda o sin límite hace que el compilador se rinda con error TS2589: Type instantiation is excessively deep and possibly infinite.
Referencia rápida
| Patrón | Significado |
|---|---|
T extends U ? X : Y | X si T se puede asignar a U; si no, Y |
T extends U ? never : T | quita los miembros que encajan con U (esto es Exclude) |
T extends U ? T : never | conserva los miembros que encajan con U (esto es Extract) |
[T] extends [U] ? X : Y | la misma comprobación, sin distribuir sobre una unión |
T extends (infer E)[] ? E : T | tipo de los elementos de un array |
T extends Promise<infer V> ? V : T | tipo del valor de una promesa |
T extends (...args: any[]) => infer R ? R : never | tipo de retorno de una función |
T extends [infer H, ...infer Rest] ? ... | primer elemento y resto de una tupla |
Preguntas frecuentes
¿Qué es un conditional type en TypeScript?
Un tipo de la forma T extends U ? X : Y. Si T se puede asignar a U, el resultado es X; si no, Y. Es un if/else para tipos que se evalúa en compilación; no queda nada de él en el JavaScript generado.
¿Qué hace la palabra clave infer en TypeScript?
infer declara una variable de tipo dentro de la cláusula extends de un conditional type y deja que TypeScript la rellene a partir del tipo que encaja. T extends Promise<infer V> ? V : T saca de un tipo promesa el tipo del valor resuelto. Solo se puede usar en la cláusula extends de un conditional type.
¿Qué es un distributive conditional type?
Cuando el tipo comprobado es un parámetro de tipo sin envolver y le pasas una unión, la condición se aplica a cada miembro por separado y los resultados se juntan. ToArray<string | number> con type ToArray<T> = T extends unknown ? T[] : never pasa a ser string[] | number[]. Envuelve los dos lados en corchetes, [T] extends [unknown], para desactivarlo.
¿Cómo obtengo el tipo de retorno de una función en TypeScript?
Usa el ReturnType<typeof fn> integrado. Es un conditional type con infer: T extends (...args: any) => infer R ? R : any. Para funciones async, envuélvelo en Awaited<...> y obtendrás el valor resuelto en lugar de la promesa.
¿Por qué IsNever<never> devuelve never y no true?
never es la unión vacía, y un distributive conditional type recorre los miembros de una unión. Si no hay miembros, no hay nada que recorrer y el resultado es never. Escribe [T] extends [never] ? true : false para comprobar never en sí.