Fuzionarea declarațiilor în TypeScript
Fuzionarea declarațiilor este ceea ce face TypeScript atunci când găsește mai multe declarații cu același nume: le combină într-o singură definiție.
Înțelegerea fuzionării declarațiilor
Fuzionarea declarațiilor îți permite să combini mai multe declarații care au același nume într-o singură definiție. Astfel poți construi tipuri complexe pas cu pas și poți extinde tipuri existente fără să pierzi siguranța tipurilor.
Beneficii principale:
- Îmbunătățire progresivă: construiești un tip treptat, în mai multe declarații.
- Extensibilitate: adaugi membri la un tip existent fără să atingi definiția lui inițială.
- Organizare: împarți un tip mare în grupuri logice.
- Compatibilitate: extinzi, la nevoie, definițiile de tipuri ale bibliotecilor externe.
Cazuri de utilizare frecvente:
- Extinderea tipurilor predefinite și a tipurilor din biblioteci externe.
- Adăugarea informațiilor de tip pentru biblioteci JavaScript.
- Organizarea interfețelor mari în mai multe fișiere.
- Crearea de API-uri fluente, cu înlănțuirea metodelor.
- Implementarea modelului de augmentare a modulelor.
Fuzionarea interfețelor
Interfețele cu același nume sunt fuzionate automat. Rezultatul conține toți membrii din toate declarațiile.
Exemplu
Cele două declarații Person devin o singură interfață cu name, age, address și email, așa că un obiect trebuie să furnizeze toate cele patru proprietăți.
interface Person din același fișier sunt fuzionate într-un singur tip.
Prima declară name: string și age: number, a doua declară address: string și email: string. Creează un obiect person cu toate cele patru proprietăți (John, 30, "123 Main St", john@example.com) și afișează întregul obiect.
Ieșire: program TypeScript și rezultatul capturat
// Prima declarație
interface Person {
name: string;
age: number;
}
// A doua declarație, cu același nume
interface Person {
address: string;
email: string;
}
// TypeScript le fuzionează în:
// interface Person {
// name: string;
// age: number;
// address: string;
// email: string;
// }
const person: Person = {
name: "John",
age: 30,
address: "123 Main St",
email: "john@example.com"
};
console.log(person);
Supraîncărcarea funcțiilor prin fuzionare
Poți scrie mai multe semnături de funcție cu același nume. Ele fuzionează într-o singură funcție supraîncărcată, iar o singură implementare tratează toate cazurile.
Exemplu
processValue are trei semnături de supraîncărcare și o singură implementare. Semnăturile îi spun apelantului exact ce tip de retur corespunde fiecărui tip de argument.
processValue — una care primește și returnează string, una number și una boolean — urmate de o singură implementare care acceptă string | number | boolean.
Implementarea transformă șirurile în majuscule, dublează numerele și neagă valorile booleene. Apeleaz-o cu "hello", 10 și true și afișează fiecare rezultat.
Ieșire: program TypeScript și rezultatul capturat
// Supraîncărcările funcției
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: boolean): boolean;
// Implementarea care tratează toate supraîncărcările
function processValue(value: string | number | boolean): string | number | boolean {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value * 2;
} else {
return !value;
}
}
// Folosirea funcției cu tipuri diferite
console.log(processValue("hello")); // "HELLO"
console.log(processValue(10)); // 20
console.log(processValue(true)); // false
Fuzionarea spațiilor de nume
Și spațiile de nume cu același nume sunt fuzionate. Membrii exportați dintr-un bloc sunt vizibili în celelalte.
Exemplu
Al doilea bloc Validation poate implementa StringValidator din primul bloc, iar codul din exterior ajunge la tot ce există prin unicul spațiu de nume fuzionat.
namespace Validation fuzionează într-un singur spațiu de nume.
Primul exportă o interfață StringValidator cu isValid(s: string): boolean; al doilea exportă o interfață NumberValidator și o clasă ZipCodeValidator care implementează StringValidator, verificând dacă șirul are exact cinci cifre. Creează un ZipCodeValidator prin Validation.ZipCodeValidator și afișează isValid pentru "12345", "1234" și "abcde".
Ieșire: program TypeScript și rezultatul capturat
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
}
namespace Validation {
export interface NumberValidator {
isValid(n: number): boolean;
}
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return s.length === 5 && /^\d+$/.test(s);
}
}
}
// După fuzionare:
// namespace Validation {
// export interface StringValidator { isValid(s: string): boolean; }
// export interface NumberValidator { isValid(n: number): boolean; }
// export class ZipCodeValidator implements StringValidator { ... }
// }
// Folosirea spațiului de nume fuzionat
const zipValidator = new Validation.ZipCodeValidator();
console.log(zipValidator.isValid("12345")); // true
console.log(zipValidator.isValid("1234")); // false
console.log(zipValidator.isValid("abcde")); // false
Fuzionarea claselor cu interfețele
O declarație de clasă poate fuziona cu o interfață care are același nume. Interfața adaugă membri la tipul clasei, iar clasa trebuie să îi implementeze.
Fuzionarea unei clase cu o interfață
Interfața Cart declară calculateTotal(), iar clasa Cart o furnizează, împreună cu propriii membri items și addItem.
// Interface declaration
interface Cart {
calculateTotal(): number;
}
// Class declaration with same name
class Cart {
items: { name: string; price: number }[] = [];
addItem(name: string, price: number): void {
this.items.push({ name, price });
}
// Must implement the interface method
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// Using the merged class and interface
const cart = new Cart();
cart.addItem("Book", 15.99);
cart.addItem("Coffee Mug", 8.99);
console.log(`Total: $${cart.calculateTotal().toFixed(2)}`);
Fuzionarea enumerărilor
Și declarațiile de enumerare cu același nume sunt fuzionate. Doar primul membru din prima declarație poate să nu aibă valoare; declarațiile ulterioare trebuie să stabilească valori explicite.
Exemplu
Direction ajunge să aibă patru membri. Căutarea inversă după valoare (Direction[0]) funcționează pe toată enumerarea fuzionată.
enum Direction fuzionează într-o singură enumerare.
Prima declară North și South (numerotate automat 0 și 1); a doua declară East = 2 și West = 3. Afișează valoarea numerică a tuturor celor patru membri, apoi afișează Direction[0] și Direction[2], ca să arăți căutarea inversă după valoare.
Ieșire: program TypeScript și rezultatul capturat
// Prima parte a enumerării
enum Direction {
North,
South
}
// A doua parte a enumerării
enum Direction {
East = 2,
West = 3
}
// După fuzionare:
// enum Direction {
// North = 0,
// South = 1,
// East = 2,
// West = 3
// }
console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3
// Se poate accesa și după valoare
console.log(Direction[0]); // "North"
console.log(Direction[2]); // "East"
Augmentarea modulelor
Poți extinde un modul sau o bibliotecă existentă declarând tipuri și funcții suplimentare cu același nume. Acest lucru se numește augmentarea modulelor.
Exemplu
Primul bloc declare namespace LibraryModule ține locul unei biblioteci externe. Al doilea bloc adaugă o interfață nouă, o proprietate opțională nouă pe User și o funcție nouă. Deoarece blocurile declare nu conțin cod care se execută, exemplul atașează o mică implementare substitut la globalThis, ca să poată rula.
declare namespace LibraryModule: primul (care ține locul unei biblioteci externe) declară o interfață User (id, name) și o funcție getUser(id: number): User; al doilea adaugă o interfață UserPreferences (theme, notifications), adaugă o proprietate opțională preferences?: UserPreferences pe User și declară getUserPreferences(userId: number): UserPreferences.
Deoarece blocurile declare nu conțin cod care se execută, atașează la globalThis o mică implementare substitut a lui LibraryModule, ca fișierul să poată rula. Apelează getUser(123) și afișează user.preferences?.theme, apoi apelează getUserPreferences(123) și afișează prefs.notifications.
Ieșire: program TypeScript și rezultatul capturat
// Definiția inițială a bibliotecii
// Imaginează-ți că vine dintr-o bibliotecă externă
declare namespace LibraryModule {
export interface User {
id: number;
name: string;
}
export function getUser(id: number): User;
}
// Augmentarea cu funcționalități suplimentare (codul tău)
declare namespace LibraryModule {
// Adaugă o interfață nouă
export interface UserPreferences {
theme: string;
notifications: boolean;
}
// Adaugă o proprietate nouă la o interfață existentă
export interface User {
preferences?: UserPreferences;
}
// Adaugă o funcție nouă
export function getUserPreferences(userId: number): UserPreferences;
}
// Implementare substitut pentru bibliotecă (în mod normal este furnizată de pachetul instalat)
(globalThis as any).LibraryModule = {
getUser: (id: number) => ({ id, name: "John", preferences: { theme: "dark", notifications: true } }),
getUserPreferences: (userId: number) => ({ theme: "dark", notifications: true })
};
// Folosirea modulului augmentat
const user = LibraryModule.getUser(123);
console.log(user.preferences?.theme);
const prefs = LibraryModule.getUserPreferences(123);
console.log(prefs.notifications);
Bune practici
Există câteva reguli de reținut atunci când folosești fuzionarea declarațiilor:
- Ordinea contează la supraîncărcarea funcțiilor: semnătura implementării ar trebui să fie cea mai generală.
- Membrii care nu sunt funcții trebuie să fie compatibili: dacă două interfețe declară o proprietate cu același nume, tipurile trebuie să fie identice sau compatibile.
- Interfețele declarate mai târziu au prioritate: dacă interfețele fuzionate intră în conflict, câștigă ultima declarație.
- Membrii private și protected: clasele nu pot fuziona dacă au membri private sau protected cu același nume, dar cu tipuri diferite.
- Exporturile din spațiile de nume: după fuzionare, doar declarațiile exportate sunt vizibile în afara spațiului de nume.
Aspecte legate de performanță
- Timpul de compilare: folosirea intensă a fuzionării declarațiilor poate încetini compilarea.
- Verificarea tipurilor: tipurile fuzionate complexe pot încetini editorul.
- Dimensiunea bundle-ului: fuzionarea declarațiilor nu are niciun efect asupra performanței la rulare sau asupra dimensiunii bundle-ului.
Sfaturi de optimizare:
- Păstrează interfețele fuzionate concentrate și coerente.
- Evită imbricarea adâncă în tipurile fuzionate.
- Folosește alias-uri
typepentru combinații simple de tipuri, în locul fuzionării.