infer în TypeScript

În ramura extends a unui tip condițional, infer X „prinde” o parte din tip și o dă un nume.

Sintaxă
type Element<T> = T extends (infer E)[] ? E : never;
type Rezultat<F> = F extends (...args: never[]) => infer R ? R : never;

Ce face infer

infer se folosește doar în condiția unui tip condițional. Spui ce formă are tipul, iar în locul părții care te interesează pui infer Nume. Dacă tipul se potrivește, Nume primește partea respectivă și o poți folosi în ramura „adevărat”.

Exemple: T extends (infer E)[] ? E : never scoate tipul elementelor unui tablou; T extends Promise<infer V> ? V : T scoate valoarea dintr-un Promise; T extends (...args: infer P) => unknown ? P : never scoate parametrii unei funcții. Așa sunt definite ReturnType, Parameters și Awaited.

Merge și în template literal types: T extends `${infer Prefix}-${infer Rest}` ? ... desparte un text. Din TypeScript 4.7 poți pune o constrângere: infer N extends number.

Exemple

Exemplul 1: Tipul elementelor și valoarea dintr-un Promise

type Element<T> = T extends readonly (infer E)[] ? E : never;
type Valoare<T> = T extends Promise<infer V> ? V : T;

const orase = ["Iași", "Cluj"];
type Oras = Element<typeof orase>;           // string

async function incarcaNote() {
    return [9, 10, 8];
}
type Note = Valoare<ReturnType<typeof incarcaNote>>;   // number[]

const o: Oras = "Brașov";
const n: Note = await incarcaNote();
console.log(o, n);

Rezultat

Brașov [ 9, 10, 8 ]

Exemplul 2: Despărțirea unui text cu infer

Tipul Ruta scoate parametrii dintr-o rută de forma /elevi/:id/note/:materie.

type Parametri<R extends string> =
    R extends `${string}:${infer P}/${infer Rest}` ? P | Parametri<`/${Rest}`> :
    R extends `${string}:${infer P}` ? P :
    never;

type P = Parametri<"/elevi/:id/note/:materie">;     // "id" | "materie"

function construieste(valori: Record<P, string>): string {
    return `/elevi/${valori.id}/note/${valori.materie}`;
}
console.log(construieste({ id: "42", materie: "informatică" }));

Rezultat

/elevi/42/note/informatică

Exemplul 3: Primul element al unui tuple

type Primul<T extends unknown[]> = T extends [infer P, ...unknown[]] ? P : undefined;

type A = Primul<[string, number]>;   // string
type B = Primul<[]>;                 // undefined

const a: A = "Ana";
const b: B = undefined;
console.log(a, b);

Rezultat

Ana undefined

Greșeli frecvente

  • Folosești infer în afara unui tip condițional. tsc dă ‘infer’ declarations are only permitted in the ‘extends’ clause of a conditional type.
  • Scrii propria variantă de ReturnType sau Awaited. Există deja în biblioteca standard.

Comenzi înrudite

  • conditional types: Un tip care alege între două variante după o condiție: T extends U ? X : Y.
  • ReturnType: ReturnType<F> dă tipul rezultatului unei funcții; pentru o funcție concretă scrii ReturnType<typeof f>.
  • Parameters: Parameters<F> dă tipurile parametrilor unei funcții, ca tuple; Parameters<F>[0] este primul parametru.
  • Awaited: Awaited<T> dă tipul valorii pe care o obții cu await din T, desfăcând și Promise-urile imbricate.
  • template literal types: Tipuri de text construite ca un template string: `id-${number}` acceptă „id-1”, „id-42” și așa mai departe.