Kodokon kodokon.com

Tipos condicionales e infer: la lógica del compilador

Domina la distributividad de los tipos condicionales, el pattern matching con infer y casos límite como never, any y boolean.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Los tipos condicionales son el mecanismo de bifurcación del sistema de tipos: T extends U ? X : Y pregunta al compilador si T es asignable a U y luego selecciona una rama. Todas las utilidades estándar como ReturnType, Exclude o Awaited se construyen sobre ellos. En este nivel, la pregunta ya no es cómo escribirlos, sino cómo predecir exactamente cómo los evalúa el compilador, especialmente cuando intervienen uniones y never.

TYPESCRIPT
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;
// true

type B = IsString<string | number>;
// boolean, not false!
Un resultado sorprendente en una unión: boolean.

El comportamiento clave es la distributividad: cuando el tipo probado es un parámetro de tipo desnudo (no envuelto en una tupla, un array o una intersección), el condicional se aplica por separado a cada miembro de la unión y luego se combinan los resultados. Por eso IsString<string | number> calcula true | false, que es boolean. Para desactivar esta distribución, envuelve ambos lados en una tupla: [T] extends [U]. Un caso límite temible: never es la unión vacía; distribuir sobre cero miembros no evalúa ninguna rama en absoluto, y el resultado es never sin importar qué rama hayas escrito.

TYPESCRIPT
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

type B = NoDist<string | number>;
// (string | number)[]

type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true thanks to the tuple
La tupla neutraliza la distribución y hace que never sea fiable.

La palabra clave infer introduce una variable de tipo capturada mediante pattern matching: el compilador intenta hacer coincidir la estructura del tipo probado con el patrón y liga la variable si la coincidencia tiene éxito. Puedes colocar varias declaraciones infer con el mismo nombre: en una posición covariante (valores producidos, como un tipo de retorno), los candidatos se fusionan en una unión; en una posición contravariante (parámetros de función), se combinan en una intersección. Esta regla se deriva directamente de la varianza: es la única opción segura en cada dirección.

TYPESCRIPT
type ElementOf<T> =
  T extends readonly (infer E)[] ? E : never;

type FirstParam<T> =
  T extends (arg: infer P) => unknown ? P : never;

type Merged<T> =
  T extends { a: (x: infer P) => void;
              b: (x: infer P) => void }
    ? P : never;

type M = Merged<{
  a: (x: { id: string }) => void;
  b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }
Dos infer en posición contravariante: intersección.

Desde TypeScript 4.7, infer acepta una restricción en línea: infer N extends number filtra la captura y, desde 4.8, el compilador incluso convierte un literal de cadena numérica en un literal de número. Otra sutileza de la especificación: un tipo condicional cuya entrada todavía contiene un parámetro genérico sin resolver permanece diferido; solo se evalúa en el sitio de instanciación, lo que explica por qué algunos errores solo aparecen en el lado de quien lo llama.

TYPESCRIPT
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">;   // never
Restricción sobre infer y conversión de literal (4.8).

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuál es el tipo Exclude<boolean, true>?
    • boolean
    • false
    • never
    • true
  2. Con type IsNever<T> = T extends never ? true : false;, ¿qué es IsNever<never>?
    • true
    • false
    • never
    • boolean
  3. Dos infer P con el mismo nombre colocados en posición de parámetro de función producen:
    • una unión de los candidatos
    • una intersección de los candidatos
    • el primer candidato encontrado
    • un error de compilación