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
| Pattern | Significato |
|---|---|
T extends U ? X : Y | X se T è assegnabile a U, altrimenti Y |
T extends U ? never : T | rimuove i membri che corrispondono a U (è Exclude) |
T extends U ? T : never | tiene i membri che corrispondono a U (è Extract) |
[T] extends [U] ? X : Y | stesso test, senza distribuire su una union |
T extends (infer E)[] ? E : T | tipo dell'elemento di un array |
T extends Promise<infer V> ? V : T | tipo del valore di una promise |
T extends (...args: any[]) => infer R ? R : never | tipo 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é.