Menu

Tipi condizionali e infer in TypeScript, con esempi

Un tipo condizionale sceglie uno tra due tipi in base a un test: T extends U ? X : Y. Scopri la sintassi, come i tipi condizionali si distribuiscono sulle union (e come impedirlo), come estrarre tipi con infer e come scrivere ReturnType da solo.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un tipo condizionale sceglie tra due tipi con un test che si legge come l'operatore ternario di JavaScript: T extends U ? X : Y. Se T è assegnabile a U, il risultato è X; altrimenti è Y.

Qui extends significa "è assegnabile a", la stessa relazione che il compilatore usa quando assegni un valore a una variabile. I tipi condizionali esistono solo in compilazione; vengono cancellati dall'output JavaScript.

La sintassi

type Result = CheckedType extends TestType ? TrueType : FalseType;

I tipi condizionali diventano utili con i generics, dove il tipo controllato è un parametro di tipo che riceverà un tipo concreto più tardi. Nel ramo true TypeScript sa che il tipo controllato soddisfa il test, quindi T["message"] qui sopra è consentito anche se T da solo non ha una proprietà message.

Si possono anche annidare, come ternari concatenati:

L'as TypeName<T> nel corpo della funzione non è facoltativo, come mostra la prossima sezione.

I tipi di ritorno condizionali richiedono un'asserzione

Una funzione il cui tipo di ritorno è un tipo condizionale sul proprio parametro di tipo non può restituire direttamente nessuno dei due rami. TypeScript non restringe T dentro il corpo, quindi non può sapere quale ramo si applica:

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>'.

Due soluzioni comuni: gli overload, che dichiarano ogni coppia input-output e controllano con precisione chi chiama, oppure un'asserzione nell'implementazione.

Gli overload sono trattati nella pagina sull'overloading delle funzioni. Con un'asserzione, il compilatore si fida di te: un ramo sbagliato nel corpo non verrebbe intercettato.

Tipi condizionali distributivi

Quando il tipo controllato è un parametro di tipo nudo e riceve una union, la condizione viene eseguita una volta per membro e i risultati vengono uniti in una nuova union:

La distribuzione è ciò che fa funzionare Exclude ed Extract. Exclude<T, U> è definito come T extends U ? never : T: ogni membro che corrisponde a U diventa never, e never sparisce da una union. Quindi Exclude<"a" | "b" | "c", "a"> è "b" | "c".

Due sorprese derivano dalla stessa regola. boolean è la union true | false, quindi ToArray<boolean> è false[] | true[], non boolean[]. E never è la union vuota, quindi un tipo condizionale distributivo che riceve never restituisce never senza testare nulla:

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

Estrarre tipi con infer

infer dichiara una nuova variabile di tipo dentro la clausola extends. Se la corrispondenza riesce, TypeScript riempie quella variabile a partire dal tipo controllato, e puoi usarla nel ramo true:

Leggi T extends Promise<infer V> ? V : T come "se T è una promise di qualcosa, chiama quel qualcosa V e restituiscilo; altrimenti restituisci T invariato". infer è consentito solo nella clausola extends di un tipo condizionale.

Una variabile infer può avere un proprio vincolo con extends. La corrispondenza riesce allora solo se il tipo dedotto lo rispetta:

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

Costruire ReturnType da solo

Il ReturnType predefinito è un tipo condizionale di una riga con infer. Scriverlo da solo è l'esercizio classico che fa scattare entrambe le idee:

typeof makeUser trasforma il valore della funzione nel suo tipo, poi il tipo condizionale lo confronta con "qualsiasi funzione" e cattura il tipo di ritorno come R. La versione della libreria standard differisce in due dettagli: il suo parametro è vincolato ai tipi funzione (T extends (...args: any) => any), quindi ReturnType<string> è un errore di compilazione invece di never, e il suo ramo falso è any. La pagina su ReturnType tratta anche Parameters, InstanceType e Awaited.

Tipi condizionali ricorsivi

Un tipo condizionale può riferirsi a se stesso, e questo gli permette di scartare qualsiasi livello di annidamento:

type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

Il predefinito Awaited<T> funziona così, scartando Promise<Promise<T>> fino a T. In pratica, tieni la ricorsione poco profonda: una ricorsione molto profonda o illimitata fa arrendere il compilatore con error TS2589: Type instantiation is excessively deep and possibly infinite.

Riferimento rapido

PatternSignificato
T extends U ? X : YX se T è assegnabile a U, altrimenti Y
T extends U ? never : Trimuove i membri che corrispondono a U (è Exclude)
T extends U ? T : nevertiene i membri che corrispondono a U (è Extract)
[T] extends [U] ? X : Ystesso test, senza distribuire su una union
T extends (infer E)[] ? E : Ttipo dell'elemento di un array
T extends Promise<infer V> ? V : Ttipo del valore di una promise
T extends (...args: any[]) => infer R ? R : nevertipo di ritorno di una funzione
T extends [infer H, ...infer Rest] ? ...primo elemento e resto di una tupla

Domande frequenti

Che cos'è un tipo condizionale in TypeScript?

Un tipo della forma T extends U ? X : Y. Se T è assegnabile a U, il risultato è X, altrimenti Y. È un if/else per i tipi, valutato in compilazione; nel JavaScript emesso non ne resta nulla.

Cosa fa la parola chiave infer in TypeScript?

infer dichiara una variabile di tipo dentro la clausola extends di un tipo condizionale e lascia che TypeScript la riempia a partire dal tipo corrispondente. T extends Promise<infer V> ? V : T estrae il tipo del valore risolto da un tipo promise. Si può usare solo nella clausola extends di un tipo condizionale.

Che cos'è un tipo condizionale distributivo?

Quando il tipo controllato è un parametro di tipo nudo e passi una union, la condizione viene applicata a ogni membro separatamente e i risultati vengono uniti. ToArray<string | number> con type ToArray<T> = T extends unknown ? T[] : never diventa string[] | number[]. Racchiudi entrambi i lati tra parentesi quadre, [T] extends [unknown], per disattivarlo.

Come si ottiene il tipo di ritorno di una funzione in TypeScript?

Usa il predefinito ReturnType<typeof fn>. È un tipo condizionale con infer: T extends (...args: any) => infer R ? R : any. Per le funzioni async, avvolgilo in Awaited<...> per ottenere il valore risolto invece della promise.

Perché IsNever<never> restituisce never invece di true?

never è la union vuota, e un tipo condizionale distributivo agisce sui membri di una union. Senza membri non c'è nulla su cui agire e il risultato è never. Scrivi [T] extends [never] ? true : false per verificare never in sé.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA