Tipuri avansate în TypeScript
TypeScript are un sistem de tipuri avansat. El îți permite să descrii cu precizie relații complexe între tipuri.
Aceste facilități te ajută să construiești aplicații robuste, ușor de întreținut și ferite de multe erori de tipuri.
Tipurile avansate din TypeScript
Această pagină oferă o prezentare generală a principalelor facilități pentru tipuri avansate:
- Tipuri mapate: transformă proprietățile unui tip existent
- Tipuri condiționale: aleg un tip în funcție de o condiție
- Tipuri literale de șablon: construiesc tipuri șir de caractere cu sintaxa de șablon
- Tipuri utilitare: ajutoare predefinite pentru transformările obișnuite
- Tipuri recursive: tipuri care se referă la ele însele, pentru date de tip arbore
- Gardieni de tip și predicate de tip: verificări care restrâng un tip în timpul execuției
- Deducerea tipurilor: potrivirea de tipare în interiorul tipurilor, cu
infer
Tipuri mapate
Un tip mapat creează un tip nou parcurgând proprietățile unui tip existent și transformând fiecare proprietate.
Tip mapat de bază
Un singur șablon transformă fiecare proprietate a unui tip obiect într-un tip nou. Aici fiecare proprietate a lui User devine boolean.
boolean fiecare proprietate a unui tip obiect.
Declară un tip generic Flags<T> mapat peste keyof T, o interfață User cu id: number, name: string și email: string, și un alias UserFlags = Flags<User>; adaugă un comentariu care arată tipul obiect echivalent. Fișierul doar declară tipuri și nu afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// Transformăm toate proprietățile în boolean
type Flags<T> = {
[K in keyof T]: boolean;
};
interface User {
id: number;
name: string;
email: string;
}
type UserFlags = Flags<User>;
// Echivalent cu:
// {
// id: boolean;
// name: boolean;
// email: boolean;
// }
Modificatori în tipurile mapate
În timpul mapării poți adăuga sau elimina modificatorii readonly și ? (opțional) pentru fiecare cheie. Un + adaugă un modificator, iar un - îl elimină.
readonly și ?.
Declară o interfață Todo (title: string, description: string, completed: boolean), un tip mapat OptionalTodo care face opționale toate proprietățile, un tip generic Concrete<T> care elimină și readonly, și ? prin -readonly și -?, și un ReadonlyRequired<T> care adaugă +readonly și elimină ?. Doar declarații de tipuri, fără rezultat afișat.
Ieșire: program TypeScript și rezultatul capturat
// Facem toate proprietățile opționale
interface Todo {
title: string;
description: string;
completed: boolean;
}
type OptionalTodo = {
[K in keyof Todo]?: Todo[K];
};
// Eliminăm modificatorii „readonly” și „?”
type Concrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
// Adăugăm „readonly” și facem obligatorii toate proprietățile
type ReadonlyRequired<T> = {
+readonly [K in keyof T]-?: T[K];
};
Redenumirea cheilor
Cu clauza as poți redenumi cheile în timpul mapării sau le poți elimina mapându-le la never. Ajutoare pentru șiruri de caractere precum Capitalize și verificările condiționale fac acest lucru posibil.
as.
Declară o interfață User (id: number, name: string, email: string), un tip generic Getters<T> care redenumește fiecare cheie în get plus numele cheii cu inițială majusculă (folosind Capitalize<string & K>), cu valoarea () => T[K], un alias UserGetters = Getters<User> cu un comentariu care arată forma rezultată, și un tip MethodsOnly<T> care păstrează doar cheile ale căror valori extind Function, mapându-le pe celelalte la never. Doar tipuri, nu afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
interface User {
id: number;
name: string;
email: string;
}
// Adăugăm un prefix la toate numele de proprietăți
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
// Filtrăm proprietățile
type MethodsOnly<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
Tipuri condiționale
Un tip condițional alege între două tipuri în funcție de o condiție. Funcționează ca o instrucțiune if, dar la nivelul tipurilor.
Tipuri condiționale de bază
Forma T extends U ? X : Y alege X atunci când T poate fi atribuit lui U, iar altfel alege Y. Verificarea are loc în întregime la nivelul tipurilor.
infer.
Declară IsString<T> ca T extends string ? true : false, apoi aliasurile A, B, C, D pentru string, number, literalul 'hello' și string | number, cu comentarii care indică rezultatele true, false, true și boolean. Declară și ArrayElement<T>, care deduce tipul elementelor U dintr-un tip tablou, plus un alias Numbers = ArrayElement<number[]>. Doar tipuri, fără rezultat afișat.
Ieșire: program TypeScript și rezultatul capturat
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true
type D = IsString<string | number>; // boolean
// Extragem tipul elementelor unui tablou
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = ArrayElement<number[]>; // number
Cuvântul cheie Infer
În interiorul condiției unui tip condițional, infer introduce o nouă variabilă de tip și captează o parte din tipul verificat. Așa funcționează ajutoarele predefinite ReturnType și Parameters.
infer captează părți dintr-un tip în interiorul unui tip condițional, recreând patru tipuri utilitare predefinite.
Declară ReturnType<T> (deduce tipul returnat R al unui tip funcție, altfel any), Parameters<T> (deduce tuplul parametrilor P, altfel never), ConstructorParameters<T> restrâns la tipuri constructor și care deduce tuplul parametrilor constructorului, și InstanceType<T>, care deduce tipul instanței dintr-un constructor. Doar tipuri, nu se afișează nimic; transformă fișierul într-un modul, ca numele să nu intre în conflict cu cele globale predefinite.
Ieșire: program TypeScript și rezultatul capturat
// Obținem tipul returnat de o funcție
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Obținem tipurile parametrilor sub formă de tuplu
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Obținem tipurile parametrilor constructorului
type ConstructorParameters<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never;
// Obținem tipul instanței dintr-un constructor
type InstanceType<T extends new (...args: any) => any> =
T extends new (...args: any) => infer R ? R : any;
// Transformăm fișierul într-un modul, ca numele de tipuri să nu intre în conflict cu cele globale predefinite
export {};
Tipuri condiționale distribuite
Atunci când tipul verificat este un simplu parametru de tip, tipul condițional se aplică separat fiecărui membru al reuniunii. Acest lucru se numește distribuire. Încadrarea tipului într-un tuplu, precum [T] extends [any], oprește distribuirea, astfel încât reuniunea este tratată ca un singur tip.
ToArrayNonDist<T> ca T extends any ? T[] : never și StrOrNumArr = ToArrayNonDist<string | number>, apoi ToArray<T> scris cu tuplul de încadrare [T] extends [any] ? T[] : never și StrOrNumArr2 = ToArray<string | number>, fiecare cu un comentariu care arată tipul rezultat. La final declară FilterStrings<T>, care păstrează doar membrii de tip șir de caractere, și un alias Letters aplicat lui 'a' | 'b' | 1 | 2 | 'c'. Doar tipuri, fără rezultat afișat.
Ieșire: program TypeScript și rezultatul capturat
// Fără distribuire
type ToArrayNonDist<T> = T extends any ? T[] : never;
type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[]
// Cu distribuire
type ToArray<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr2 = ToArray<string | number>; // string[] | number[]
// Filtrăm tipurile care nu sunt șiruri de caractere
type FilterStrings<T> = T extends string ? T : never;
type Letters = FilterStrings<'a' | 'b' | 1 | 2 | 'c'>; // 'a' | 'b' | 'c'
Tipuri literale de șablon
Tipurile literale de șablon construiesc tipuri noi de șiruri de caractere folosind aceeași sintaxă cu accent grav ca șirurile șablon din JavaScript.
Tipuri literale de șablon de bază
Un tip literal de șablon limitează un șir de caractere la un tipar. Combinat cu reuniuni, produce toate combinațiile posibile ale părților. Atribuirea unui șir care nu se potrivește cu tiparul este o eroare de compilare.
Greeting egal cu șablonul `Hello, ${string}`, atribuie valoarea validă 'Hello, World!' lui validGreeting și atribuie 'Hi there!' lui invalidGreeting, marcat cu // Eroare, astfel încât fișierul să nu treacă intenționat verificarea tipurilor. Apoi declară reuniunile Color ('red' | 'green' | 'blue') și Size ('small' | 'medium' | 'large') și un tip șablon Style egal cu `${Color}-${Size}`, cu un comentariu care enumeră cele nouă valori rezultate. Rezultat așteptat: o eroare de compilare pentru salutul nevalid.
Ieșire: program TypeScript și rezultatul capturat
type Greeting = `Hello, ${string}`;
const validGreeting: Greeting = 'Hello, World!';
const invalidGreeting: Greeting = 'Hi there!'; // Eroare
// Cu reuniuni
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';
type Style = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | 'red-large' |
// 'green-small' | 'green-medium' | 'green-large' |
// 'blue-small' | 'blue-medium' | 'blue-large'
Tipuri pentru manipularea șirurilor de caractere
TypeScript oferă patru ajutoare care schimbă felul scrierii pentru tipurile literale de șir: Uppercase, Lowercase, Capitalize și Uncapitalize. Ele sunt utile în interiorul tipurilor literale de șablon.
T1 la T4 folosind Uppercase<'hello'>, Lowercase<'WORLD'>, Capitalize<'typescript'> și Uncapitalize<'TypeScript'>, cu comentarii care arată rezultatele, apoi o reuniune EventType ('click' | 'change' | 'keydown') și un tip șablon EventHandler care adaugă prefixul on la numele evenimentelor scrise cu inițială majusculă. Doar tipuri, fără rezultat afișat.
Ieșire: program TypeScript și rezultatul capturat
// Tipuri predefinite pentru manipularea șirurilor de caractere
type T1 = Uppercase<'hello'>; // 'HELLO'
type T2 = Lowercase<'WORLD'>; // 'world'
type T3 = Capitalize<'typescript'>; // 'Typescript'
type T4 = Uncapitalize<'TypeScript'>; // 'typeScript'
// Creăm un tip pentru gestionarii de evenimente
type EventType = 'click' | 'change' | 'keydown';
type EventHandler = `on${Capitalize<EventType>}`;
// 'onClick' | 'onChange' | 'onKeydown'
Tipare avansate
Tipurile literale de șablon pot fi combinate cu infer și cu redenumirea cheilor. Astfel poți extrage informații dintr-un tip șir de caractere, precum parametrii unei rute, sau poți genera un întreg API de nume de gestionari pornind de la o hartă de evenimente.
infer și redenumirea cheilor.
Declară un tip recursiv ExtractRouteParams<T> care se potrivește cu `${string}:${infer Param}/${infer Rest}` (și cu forma cu un singur parametru), pentru a construi un tip obiect cu câte o proprietate string pentru fiecare parametru al rutei, plus un alias Params pentru '/users/:userId/posts/:postId'. Apoi declară un tip EventMap (click cu numerele x/y, change ca string, keydown cu key: string și code: number) și un tip mapat EventHandlers care redenumește fiecare cheie în on plus numele cu inițială majusculă și îi dă tipul unui gestionar care primește EventMap[K]. Doar tipuri, fără rezultat afișat.
Ieșire: program TypeScript și rezultatul capturat
// Extragem parametrii rutei
type ExtractRouteParams<T> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<`${Rest}`>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string; }
// Creăm un emițător de evenimente sigur din punct de vedere al tipurilor
type EventMap = {
click: { x: number; y: number };
change: string;
keydown: { key: string; code: number };
};
type EventHandlers = {
[K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void;
};
Tipuri utilitare
TypeScript vine cu multe tipuri utilitare predefinite. Ele acoperă cele mai obișnuite transformări de tipuri, ca să nu fie nevoie să le scrii singur.
Tipuri utilitare uzuale
Partial, Required, Readonly, Pick și Omit sunt cele pe care le vei folosi cel mai des. Poți citi și tipul unei singure proprietăți cu User['id'], precum și toate numele de chei cu keyof User.
User cu id: number, name: string, email: string și createdAt: Date, apoi aliasurile PartialUser = Partial<User>, RequiredUser = Required<PartialUser>, ReadonlyUser = Readonly<User>, UserPreview = Pick<User, 'id' | 'name'> și UserWithoutEmail = Omit<User, 'email'>, fiecare cu un comentariu scurt. Încheie cu UserId = User['id'] și UserKeys = keyof User și cu comentarii care arată rezultatele. Doar tipuri, fără rezultat afișat.
Ieșire: program TypeScript și rezultatul capturat
// Tipuri de bază
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
// Facem toate proprietățile opționale
type PartialUser = Partial<User>;
// facem toate proprietățile obligatorii
type RequiredUser = Required<PartialUser>;
// facem toate proprietățile doar pentru citire
type ReadonlyUser = Readonly<User>;
// alegem anumite proprietăți
type UserPreview = Pick<User, 'id' | 'name'>;
// omitem anumite proprietăți
type UserWithoutEmail = Omit<User, 'email'>;
// extragem tipurile proprietăților
type UserId = User['id']; // number
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'createdAt'
Tipuri utilitare avansate
Exclude elimină membri dintr-o reuniune, iar Extract păstrează doar pe cei care se potrivesc. Poți scrie și propriile ajutoare, cum ar fi un tip Mutable care elimină readonly de la fiecare proprietate.
NonNullable<T> care transformă null | undefined în never, o reuniune Numbers egală cu 1 | 2 | 3 | 'a' | 'b', JustNumbers = Exclude<Numbers, string> și JustStrings = Extract<Numbers, string> cu comentarii care arată rezultatele, apoi tipurile obiect A (a: string; b: number; c: boolean) și B (a: string; b: number) și C = Omit<A, keyof B>, iar la final un tip mapat Mutable<T> care folosește -readonly. Doar tipuri, fără rezultat afișat; transformă fișierul într-un modul, ca NonNullable să nu intre în conflict cu cel global.
Ieșire: program TypeScript și rezultatul capturat
// Creăm un tip care exclude null și undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// Excludem tipuri dintr-o reuniune
type Numbers = 1 | 2 | 3 | 'a' | 'b';
type JustNumbers = Exclude<Numbers, string>; // 1 | 2 | 3
// Extragem tipuri dintr-o reuniune
type JustStrings = Extract<Numbers, string>; // 'a' | 'b'
// Obținem tipul care nu se află în al doilea tip
type A = { a: string; b: number; c: boolean };
type B = { a: string; b: number };
type C = Omit<A, keyof B>; // { c: boolean }
// Creăm un tip cu toate proprietățile modificabile
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Transformăm fișierul într-un modul, ca numele de tipuri să nu intre în conflict cu cele globale predefinite
export {};
Tipuri recursive
Un tip recursiv se referă la el însuși în propria definiție. Aceasta este modalitatea naturală de a descrie date de tip arbore, în care un nod poate conține alte noduri de aceeași formă.
Tip recursiv de bază
Un nod de arbore binar conține nodurile opționale left și right, de același tip. O valoare JSON poate conține tablouri și obiecte cu alte valori JSON. Un comentariu conține un tablou de răspunsuri care sunt tot comentarii.
BinaryTree<T> cu o valoare value și subarbori opționali left/right de același tip, o reuniune JSONValue formată din string, number, boolean, null, JSONValue[] și un obiect cu valori JSONValue, și un tip Comment cu id, content, un tablou replies: Comment[] și createdAt: Date. Doar tipuri, fără rezultat afișat; transformă fișierul într-un modul, ca Comment să nu intre în conflict cu tipul global din DOM.
Ieșire: program TypeScript și rezultatul capturat
// Arbore binar simplu
type BinaryTree<T> = {
value: T;
left?: BinaryTree<T>;
right?: BinaryTree<T>;
};
// Structură de date asemănătoare cu JSON
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// Comentarii imbricate
type Comment = {
id: number;
content: string;
replies: Comment[];
createdAt: Date;
};
// Transformăm fișierul într-un modul, ca numele de tipuri să nu intre în conflict cu cele globale predefinite
export {};
Tipuri recursive avansate
Aceeași idee descrie o listă înlănțuită, un arbore de directoare ai cărui copii sunt fișiere sau alte directoare, o mașină cu stări ale cărei tranziții duc la alte stări și chiar un tip de funcție care se acceptă pe sine.
LinkedList<T> cu value și next: LinkedList<T> | null, un tip File (type: 'file', name, size) și un tip Directory (type: 'directory', name, children: (File | Directory)[]), un tip State ale cărui transitions asociază numele evenimentelor cu alte State, și un tip de funcție RecursiveFunction<T> care acceptă T | RecursiveFunction<T>. Doar tipuri, fără rezultat afișat; transformă fișierul într-un modul, ca File să nu intre în conflict cu tipul global din DOM.
Ieșire: program TypeScript și rezultatul capturat
// Tip pentru o listă înlănțuită
type LinkedList<T> = {
value: T;
next: LinkedList<T> | null;
};
// Tip pentru o structură de directoare
type File = {
type: 'file';
name: string;
size: number;
};
type Directory = {
type: 'directory';
name: string;
children: (File | Directory)[];
};
// Tip pentru o mașină cu stări
type State = {
value: string;
transitions: {
[event: string]: State;
};
};
// Tip pentru o funcție recursivă
type RecursiveFunction<T> = (x: T | RecursiveFunction<T>) => void;
// Transformăm fișierul într-un modul, ca numele de tipuri să nu intre în conflict cu cele globale predefinite
export {};
Bune practici
Când să folosești tipuri avansate:
- Folosește tipuri mapate atunci când trebuie să schimbi dintr-odată multe proprietăți ale unui tip obiect
- Folosește tipuri condiționale atunci când un tip depinde de alt tip
- Folosește tipuri literale de șablon pentru tipare de șiruri de caractere și pentru potrivirea lor
- Folosește tipuri utilitare pentru transformările obișnuite — preferă-le pe cele predefinite atunci când se potrivesc
- Folosește tipuri recursive pentru date de tip arbore sau imbricate
Aspecte legate de performanță:
- Tipurile recursive imbricate pe multe niveluri pot încetini compilatorul TypeScript
- Tipurile reuniune foarte mari (peste 100 de membri) pot cauza probleme de performanță
- Împarte tipurile complexe în aliasuri de tip mai mici
Capcane frecvente
Probleme legate de deducerea tipurilor:
- Tipurile condiționale se distribuie peste tipurile reuniune, ceea ce poate fi surprinzător
inferse comportă diferit în funcție de locul în care este folosit- Unele tipuri utilitare nu funcționează bine cu
anysauunknown
Ușurința întreținerii:
- Folosirea excesivă a tipurilor complexe face codul greu de citit
- Explică transformările complexe de tipuri prin comentarii
- Pentru cazurile foarte complexe, o aserțiune de tip sau o funcție ajutătoare poate fi mai simplă