Tipuri condiționale în TypeScript
Un tip condițional este un tip care depinde de alt tip. Funcționează ca o instrucțiune if-else, dar pentru tipuri în loc de valori.
Înțelegerea tipurilor condiționale în TypeScript
Tipurile condiționale îți permit să construiești tipuri care se schimbă în funcție de tipul pe care îl primesc. Ele fac posibile transformările avansate de tipuri și programarea la nivel de tipuri.
Concepte cheie:
- Logică la nivel de tipuri: rulează o condiție pe un tip
- Inferența tipurilor: descompune tipurile cu
infer - Compunere: combină-le cu alte facilități TypeScript
- Tipuri utilitare: construiește ajutoare de tip reutilizabile
Cazuri de utilizare frecvente:
- Supraîncărcarea funcțiilor cu siguranță de tip
- Transformarea tipurilor de răspuns ale unui API
- Validare complexă a tipurilor
- Utilitare de tip reutilizabile
- Inferență avansată a tipurilor
Sintaxa de bază a tipurilor condiționale
Un tip condițional are forma T extends U ? X : Y. Citește-l astfel: dacă tipul T extinde (este asignabil la) tipul U, folosește tipul X, altfel folosește tipul Y.
Exemplu
IsString<T> devine true pentru string și pentru literalul "hello", deoarece un literal extinde tipul său de bază. Pentru number devine false. Și variabilele pot folosi tipul rezultat.
T extends U ? X : Y.
Declară IsString<T> ca T extends string ? true : false, apoi aliasurile Result1, Result2 și Result3 pentru string, number și literalul "hello", cu comentarii care dau true, false, true, și două variabile a: IsString<string> și b: IsString<number> cu comentarii care notează tipurile lor 'true' și 'false'. Doar tipuri, nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
type IsString<T> = T extends string ? true : false;
// Exemple de utilizare
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
type Result3 = IsString<"hello">; // true (tipurile literale extind tipurile lor de bază)
// Putem folosi acest lucru și cu variabile
let a: IsString<string>; // a are tipul 'true'
let b: IsString<number>; // b are tipul 'false'
Tipuri condiționale cu uniuni
Tipurile condiționale sunt deosebit de utile împreună cu tipurile uniune. Când tipul verificat este o uniune, condiția se aplică separat fiecărui membru. Acest lucru se numește tip condițional distributiv.
Tipuri condiționale distributive
ToArray<string | number> este împărțit în ToArray<string> | ToArray<number>, ceea ce dă string[] | number[]. Aceeași distribuție îi permite lui ExtractString să păstreze doar membrii de tip string ai unei uniuni, mapându-i pe ceilalți la never.
ToArray<T> ca T extends any ? T[] : never și StringOrNumberArray = ToArray<string | number> cu comentarii care explică faptul că devine string[] | number[], apoi ExtractString<T> care păstrează doar membrii de tip string și StringsOnly aplicat pe string | number | boolean | "hello" cu un comentariu care dă string | "hello". Doar tipuri, nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
type ToArray<T> = T extends any ? T[] : never;
// Când este folosit cu un tip uniune, se aplică fiecărui membru al uniunii
type StringOrNumberArray = ToArray<string | number>;
// Aceasta devine ToArray<string> | ToArray<number>
// Care devine string[] | number[]
// Putem, de asemenea, să extragem anumite tipuri dintr-o uniune
type ExtractString<T> = T extends string ? T : never;
type StringsOnly = ExtractString<string | number | boolean | "hello">;
// Rezultat: string | "hello"
Inferența tipurilor cu infer
Cuvântul cheie infer declară o variabilă de tip în interiorul condiției unui tip condițional. Dacă condiția se potrivește, variabila conține tipul capturat și îl poți folosi în ramura adevărată.
Extragerea tipurilor din structuri complexe
ReturnType capturează tipul returnat R al unui tip de funcție. ElementType capturează tipul elementelor U al unui tip tablou. Cu typeof greet poți trimite tipul unei funcții reale către aceste ajutoare.
infer care capturează un tip returnat și tipul elementelor unui tablou.
Declară ReturnType<T> ca T extends (...args: any[]) => infer R ? R : never, două funcții greet() care returnează "Salut, lume!" și getNumber() care returnează 42, și aliasurile GreetReturnType/NumberReturnType folosind typeof, cu comentarii care dau string și number. Apoi declară ElementType<T> care deduce U din (infer U)[] și aliasuri pentru number[] și string[]. Doar tipuri, nu se afișează nimic; fă din fișier un modul, astfel încât ReturnType local să nu intre în conflict cu cel global.
Ieșire: program TypeScript și rezultatul capturat
// Extrage tipul returnat al unui tip de funcție
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Exemple
function greet() { return "Salut, lume!"; }
function getNumber() { return 42; }
type GreetReturnType = ReturnType<typeof greet>; // string
type NumberReturnType = ReturnType<typeof getNumber>; // number
// Extrage tipul elementelor dintr-un tablou
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayElement = ElementType<number[]>; // number
type StringArrayElement = ElementType<string[]>; // string
// Facem din acest fișier un modul, astfel încât ReturnType local să nu intre în conflict cu cel global predefinit
export {};
Tipuri condiționale predefinite
Biblioteca standard a TypeScript include deja mai multe tipuri condiționale, așa că nu trebuie să le scrii singur pe cele obișnuite.
Utilitare din biblioteca standard
Extract păstrează membrii unei uniuni care se potrivesc, Exclude îi elimină, NonNullable înlătură null și undefined, iar Parameters și ReturnType descompun un tip de funcție.
OnlyStrings = Extract<string | number | boolean, string>, NoStrings = Exclude<string | number | boolean, string>, NotNull = NonNullable<string | null | undefined>, Params = Parameters<(a: string, b: number) => void> și Return = ReturnType<() => string>, fiecare cu un comentariu care explică utilitarul și tipul rezultat. Doar tipuri, nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// Extract<T, U> - Extrage din T tipurile care sunt asignabile la U
type OnlyStrings = Extract<string | number | boolean, string>; // string
// Exclude<T, U> - Exclude din T tipurile care sunt asignabile la U
type NoStrings = Exclude<string | number | boolean, string>; // number | boolean
// NonNullable<T> - Elimină null și undefined din T
type NotNull = NonNullable<string | null | undefined>; // string
// Parameters<T> - Extrage tipurile parametrilor dintr-un tip de funcție
type Params = Parameters<(a: string, b: number) => void>; // [string, number]
// ReturnType<T> - Extrage tipul returnat dintr-un tip de funcție
type Return = ReturnType<() => string>; // string
Tipare și tehnici avansate
Tipurile condiționale se pot apela pe ele însele, pot fi înlănțuite ca o scară if-else și pot descrie tipul returnat al unei funcții generice.
Tipuri condiționale recursive
Un tip condițional se poate referi la sine însuși. UnwrapPromise continuă să despacheteze până când tipul nu mai este un Promise, astfel încât chiar și o promisiune aflată într-o promisiune se rezolvă la tipul interior simplu.
Promise.
Declară UnwrapPromise<T> ca T extends Promise<infer U> ? UnwrapPromise<U> : T, apoi aliasurile A, B și C pentru Promise<string>, Promise<Promise<number>> și boolean cu comentarii care dau string, number și boolean. Doar tipuri, nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// Despachetează în profunzime tipurile Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
// Exemple
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<boolean>; // boolean
Lanțuri if-else la nivel de tipuri
Mai multe condiții pot fi înlănțuite, una după alta. TypeName verifică un tip, pe rând, față de string, number, boolean, undefined și Function, iar în lipsa unei potriviri revine la "object".
TypeName<T> care mapează string, number, boolean, undefined și Function la literalele de tip șir "string", "number", "boolean", "undefined" și "function", revenind la "object". Adaugă aliasurile de la T0 la T4 pentru string, 42, true, () => void și Date[] cu comentarii care arată rezultatele. Doar tipuri, nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
// Utilizare
type T0 = TypeName<string>; // "string"
type T1 = TypeName<42>; // "number"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<Date[]>; // "object"
Tipuri returnate condiționale
Tipurile condiționale strălucesc în utilitarele generice și în bibliotecile cu siguranță de tip. Aici, tipul returnat al unei funcții generice depinde de tipul argumentului ei. În interiorul funcției este nevoie de as any, deoarece TypeScript nu poate verifica corpul față de un tip returnat condițional.
processValue<T>(value: T) care returnează string pentru șiruri, number pentru numere, boolean pentru valori booleene și never în rest; transformă șirurile în majuscule, dublează numerele, neagă valorile booleene (fiecare cu o aserțiune as any) și aruncă Tip nesuportat pentru orice altceva. Apeleaz-o cu "hello", 10 și true, stocând stringResult, numberResult și boolResult cu comentarii care notează "HELLO", 20 și false. Nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// O funcție care returnează tipuri diferite în funcție de tipul de intrare
function processValue<T>(value: T): T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: never {
if (typeof value === "string") {
return value.toUpperCase() as any; // Aserțiunea de tip este necesară din cauza limitărilor
} else if (typeof value === "number") {
return (value * 2) as any;
} else if (typeof value === "boolean") {
return (!value) as any;
} else {
throw new Error("Tip nesuportat");
}
}
// Utilizare
const stringResult = processValue("hello"); // Returnează "HELLO" (tipul este string)
const numberResult = processValue(10); // Returnează 20 (tipul este number)
const boolResult = processValue(true); // Returnează false (tipul este boolean)
Bune practici
Ce să faci:
- Folosește tipuri condiționale pentru transformări complexe de tipuri
- Combină-le cu
inferpentru a extrage părți dintr-un tip - Creează utilitare de tip reutilizabile
- Documentează tipurile condiționale complexe
- Testează cazurile limită ale definițiilor tale de tipuri
Ce să nu faci:
- Nu recurge la tipuri condiționale complexe atunci când un tip simplu este suficient
- Nu construi tipuri condiționale adânc imbricate, greu de citit
- Nu uita că tipurile foarte complexe încetinesc compilatorul
- Nu folosi tipuri condiționale pentru logica de la rulare — ele există doar la compilare
Considerații de performanță:
- Tipurile condiționale adânc imbricate cresc timpii de compilare
- Stochează rezultatele intermediare în aliasuri de tip
- Ține cont de limitele de adâncime a recursivității din TypeScript