Inferența tipurilor în TypeScript
Inferența tipurilor înseamnă că TypeScript deduce singur un tip, din valoarea pe care o atribui sau din locul în care este folosită o expresie. Obții verificarea tipurilor fără să scrii de mână fiecare tip.
Cum funcționează inferența tipurilor în TypeScript
Inferența tipurilor este capacitatea TypeScript de a găsi și de a atribui singur tipuri variabilelor, rezultatelor funcțiilor și expresiilor, pe baza modului în care sunt folosite, fără să scrii adnotări explicite de tip.
Astfel codul rămâne scurt și, în același timp, sigur din punctul de vedere al tipurilor.
Concepte cheie:
- Inferența tipurilor: tipul este detectat automat din valoarea atribuită
- Tipare contextuală: tipul este dedus din codul înconjurător
- Cel mai bun tip comun: regula folosită de TypeScript pentru a găsi un singur tip care se potrivește mai multor valori (de exemplu într-un tablou)
- Lărgire / restrângere: tipurile devin mai largi sau mai precise, în funcție de modul în care este folosită o valoare
- Când se întâmplă: la inițializarea variabilelor, la valorile returnate, la parametrii impliciți, la callback-uri și la literali
Exemplu
Niciuna dintre aceste variabile nu are o adnotare de tip, totuși fiecare primește un tip precis din valoarea ei. Pentru că name este un string, toUpperCase() funcționează, iar toFixed() ar fi o eroare.
name = "Alice", age = 30, isActive = true, numbers = [1, 2, 3] și mixed = [1, "two", true] cu let, notând în comentarii tipul dedus al fiecăreia (string, number, boolean, number[], (string | number | boolean)[]). Apelează name.toUpperCase() și age.toFixed(2) pentru a arăta că tipurile deduse pot fi folosite și lasă name.toFixed(2) comentat, cu o notă că ar fi o eroare. Nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// TypeScript deduce singur tipurile acestor variabile
let name = "Alice"; // dedus ca string
let age = 30; // dedus ca number
let isActive = true; // dedus ca boolean
let numbers = [1, 2, 3]; // dedus ca number[]
let mixed = [1, "two", true]; // dedus ca (string | number | boolean)[]
// Folosirea tipurilor deduse
name.toUpperCase(); // Funcționează pentru că name este dedus ca string
age.toFixed(2); // Funcționează pentru că age este dedus ca number
// name.toFixed(2); // Eroare: Proprietatea 'toFixed' nu există pe tipul 'string'
// Transformăm fișierul în modul, ca variabila locală `name` să nu intre în conflict cu cea globală predefinită
export {};
Inferența tipului returnat de funcții
TypeScript poate deduce tipul returnat de o funcție analizând instrucțiunile ei return. Dacă ramuri diferite returnează tipuri diferite, rezultatul este o uniune a acestora.
Exemplu
Se deduce că greet returnează string, că add returnează number și că getValue returnează string | number, pentru că cele două ramuri ale ei returnează tipuri diferite.
return.
Scrie greet(name: string) care returnează un șir de tip template (dedus ca string), add(a: number, b: number) care returnează suma lor (dedus ca number) și getValue(key: string) care returnează "Alice" când cheia este "name" și 42 în rest (dedus ca string | number). Pune rezultatele apelurilor greet("Bob"), add(5, 3) și getValue("age") în variabile, cu comentarii care numesc tipurile deduse. Nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// Tipul returnat este dedus ca string
function greet(name: string) {
return `Salut, ${name}!`;
}
// Tipul returnat este dedus ca number
function add(a: number, b: number) {
return a + b;
}
// Tipul returnat este dedus ca string | number
function getValue(key: string) {
if (key === "name") {
return "Alice";
} else {
return 42;
}
}
// Folosirea tipurilor returnate deduse
let greeting = greet("Bob"); // dedus ca string
let sum = add(5, 3); // dedus ca number
let value = getValue("age"); // dedus ca string | number
Tipare contextuală
TypeScript poate deduce un tip și din contextul în care apare o expresie. Un callback transmis unei metode de tablou sau unui ascultător de evenimente își primește tipurile parametrilor de la acea metodă, așa că nu trebuie să le adnotezi.
Exemplu
Parametrul name din forEach și map este dedus ca string, pentru că names este un string[], iar nameLengths devine number[]. În gestionarul de clic, event este dedus ca MouseEvent. Ascultătorul este împachetat într-o verificare pentru document, ca fișierul să ruleze și în afara browserului.
names cu "Alice", "Bob" și "Charlie". În names.forEach afișează fiecare nume scris cu majuscule (parametrul name este dedus ca string), apoi construiește nameLengths cu names.map, returnând lungimea fiecărui nume (dedus number[]). La final înregistrează un gestionar document.addEventListener("click", ...) care afișează event.clientX și event.clientY (parametrul event este dedus ca MouseEvent), protejat de o verificare typeof document !== "undefined", ca fișierul să ruleze și în afara browserului.
Ieșire: program TypeScript și rezultatul capturat
// Tipul parametrului din callback este dedus din contextul metodei de tablou
const names = ["Alice", "Bob", "Charlie"];
// Parametrul 'name' este dedus ca string
names.forEach(name => {
console.log(name.toUpperCase());
});
// Parametrul 'name' este dedus ca string, iar tipul returnat este dedus ca number
const nameLengths = names.map(name => {
return name.length;
});
// nameLengths este dedus ca number[]
// Tipurile parametrilor din gestionarele de evenimente sunt și ele deduse
// (protejat printr-o verificare, ca fișierul să ruleze și în afara browserului, unde `document` nu există)
if (typeof document !== "undefined") {
document.addEventListener("click", event => {
// 'event' este dedus ca MouseEvent
console.log(event.clientX, event.clientY);
});
}
Inferența tipurilor la obiectele literale
Când creezi un obiect literal, TypeScript deduce tipul fiecărei proprietăți, inclusiv al obiectelor imbricate. Accesarea unei proprietăți care nu există este depistată la compilare.
Exemplu
Obiectul user primește o formă dedusă completă, așa că se știe că user.details.address.city este un string. Liniile comentate arată accesări pe care TypeScript le-ar respinge.
user cu id: 1, name: "Alice", email: "alice@example.com", active: true și un obiect imbricat details care conține age: 30 și un address cu city: "New York" și country: "USA". Afișează user.name.toUpperCase(), user.details.age.toFixed(0) și user.details.address.city.toLowerCase() și lasă comentate trei accesări la proprietăți care nu există (user.age, user.details.name, user.details.address.zip), notând erorile pe care le-ar produce.
Ieșire: program TypeScript și rezultatul capturat
// TypeScript deduce tipul acestui obiect
const user = {
id: 1,
name: "Alice",
email: "alice@example.com",
active: true,
details: {
age: 30,
address: {
city: "New York",
country: "USA"
}
}
};
// Accesarea proprietăților deduse
console.log(user.name.toUpperCase());
console.log(user.details.age.toFixed(0));
console.log(user.details.address.city.toLowerCase());
// Erorile de tip ar fi depistate
// console.log(user.age); // Eroare: Proprietatea 'age' nu există pe tipul '...'
// console.log(user.details.name); // Eroare: Proprietatea 'name' nu există pe tipul '...'
// console.log(user.details.address.zip); // Eroare: Proprietatea 'zip' nu există pe tipul '...'
Tipare avansate
Alte două tipare schimbă comportamentul inferenței: aserțiunile as const fac tipurile deduse cât mai restrânse posibil, iar analiza fluxului de control restrânge un tip uniune în interiorul blocurilor if și switch.
Aserțiuni const
Un let name = "Alice" obișnuit este lărgit la string. Cu as const tipul devine exact literalul "Alice", iar un obiect marcat as const devine în întregime doar-citire, cu tipuri literale pentru proprietăți.
as const.
Declară let name = "Alice" (lărgit la string) și const nameConst = "Alice" as const (restrâns la literalul "Alice"). Apoi creează un obiect user cu id: 1, name: "Alice" și un tablou roles cu "admin" și "user" marcat as const (un tuplu doar-citire), întregul obiect fiind și el marcat as const. Lasă user.name = "Bob" comentat, cu o notă că ar fi o eroare de proprietate doar-citire. Nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// Inferență de tip obișnuită (se lărgește la string)
let name = "Alice"; // tip: string
// Aserțiune const (se restrânge la tipul literal)
const nameConst = "Alice" as const; // tip: "Alice"
// Cu obiecte
const user = {
id: 1,
name: "Alice",
roles: ["admin", "user"] as const // tuplu doar-citire
} as const;
// user.name = "Bob"; // Eroare: Nu se poate atribui lui 'name' pentru că este o proprietate doar-citire
// Transformăm fișierul în modul, ca variabila locală `name` să nu intre în conflict cu cea globală predefinită
export {};
Gărzi de tip și analiza fluxului de control
În interiorul if (typeof value === "string") tipul lui value este restrâns la string, iar în ramura else la number. În area, switch-ul pe proprietatea kind restrânge shape la Circle sau Square, așa că radius și size pot fi folosite în siguranță.
typeof și o uniune discriminată.
Scrie processValue(value: string | number) care afișează valoarea cu majuscule când typeof value === "string" și value.toFixed(2) în rest. Apoi declară interfețele Circle (kind: "circle", radius) și Square (kind: "square", size), o uniune Shape și o funcție area(shape: Shape) al cărei switch pe shape.kind returnează Math.PI * radius ** 2 pentru cercuri și size ** 2 pentru pătrate. Doar declară funcțiile; nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
function processValue(value: string | number) {
// În acest bloc tipul este restrâns la string
if (typeof value === "string") {
console.log(value.toUpperCase());
}
// Aici tipul este restrâns la number
else {
console.log(value.toFixed(2));
}
}
// Uniuni discriminate
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; size: number; }
type Shape = Circle | Square;
function area(shape: Shape) {
// Tipul este restrâns pe baza proprietății 'kind'
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
}
}
Bune practici
Iată câteva bune practici pentru lucrul cu inferența tipurilor din TypeScript:
Exemplu
Lasă valorile simple să fie deduse, scrie întotdeauna tipurile parametrilor funcțiilor, adaugă tipuri returnate la funcțiile complexe, adnotează tablourile și obiectele goale (altfel devin any[] sau {}) și folosește o aserțiune de tip atunci când TypeScript nu poate cunoaște tipul exact. Căutarea în DOM este protejată printr-o verificare, ca fișierul să ruleze și în afara browserului.
- Lasă un simplu
let message = "Salut"să fie dedus. 2) Dă-i luiformatName(firstName: string, lastName: string)tipuri explicite pentru parametri. 3) Dă-i luiprocessData(input: string[])un tip returnat explicit{ count: number; items: string[] }și returnează numărul de elemente plus elementele curățate de spații. 4) Adnotează un tablou gol castring[]și un obiect gol caRecord<string, unknown>. 5) Folosește o aserțiune de tipas HTMLCanvasElementpedocument.getElementById("main-canvas"), protejată de o verificaretypeof document !== "undefined", ca fișierul să ruleze și în afara browserului. Nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// 1. Lasă TypeScript să deducă tipurile simple
let message = "Salut"; // Bine: aici nu este nevoie de un tip explicit
// 2. Scrie tipuri explicite pentru parametrii funcțiilor
function formatName(firstName: string, lastName: string) {
return `${firstName} ${lastName}`;
}
// 3. Ia în calcul adăugarea tipului returnat pentru funcțiile complexe
function processData(input: string[]): { count: number; items: string[] } {
return {
count: input.length,
items: input.map(item => item.trim())
};
}
// 4. Folosește tipuri explicite pentru tablourile și obiectele goale
const emptyArray: string[] = []; // Fără adnotare ar fi dedus ca any[]
const configOptions: Record<string, unknown> = {}; // Fără adnotare ar fi dedus ca {}
// 5. Folosește aserțiuni de tip atunci când TypeScript nu poate deduce corect
// (protejat printr-o verificare, ca fișierul să ruleze și în afara browserului, unde `document` nu există)
if (typeof document !== "undefined") {
const canvas = document.getElementById("main-canvas") as HTMLCanvasElement;
}
Când să folosești tipuri explicite
Inferența tipurilor este puternică, dar în unele situații o adnotare explicită de tip este alegerea mai bună.
Recomandat pentru tipuri explicite:
- Contracte de API public: parametrii și tipurile returnate ale funcțiilor din codul de bibliotecă
- Tipuri complexe: când tipul dedus este prea larg sau greu de citit
- Documentare: pentru ca acel cod să se explice singur
- Siguranța tipurilor: când trebuie să impui constrângeri specifice
- Colecții goale: tablouri sau obiecte goale care sunt completate mai târziu
Tipurile explicite pentru valorile returnate complexe și pentru obiectele de configurare ajută compilatorul și editorul să rămână rapide, pentru că nu mai trebuie recalculate din modul de utilizare.
Tipuri explicite pentru valori complexe
Trei cazuri în care merită scrisă o adnotare explicită: un tip returnat complex, un tablou gol care va fi completat mai târziu și un obiect de configurare.
// Good: Explicit type for complex return values
function processData(input: string[]): { results: string[]; count: number } {
return {
results: input.map(processItem),
count: input.length
};
}
// Good: Explicit type for empty arrays
const items: Array<{ id: number; name: string }> = [];
// Good: Explicit type for configuration objects
const config: {
apiUrl: string;
retries: number;
timeout: number;
} = {
apiUrl: "https://api.example.com",
retries: 3,
timeout: 5000
};