Română

Explorați tipurile branded în TypeScript, o tehnică puternică pentru a obține tipizare nominală într-un sistem de tipuri structural. Învățați cum să sporiți siguranța tipurilor și claritatea codului.

Tipuri Branded în TypeScript: Tipizare Nominală într-un Sistem Structural

Sistemul de tipuri structural al TypeScript oferă flexibilitate, dar uneori poate duce la comportamente neașteptate. Tipurile branded oferă o modalitate de a impune tipizarea nominală, sporind siguranța tipurilor și claritatea codului. Acest articol explorează în detaliu tipurile branded, oferind exemple practice și bune practici pentru implementarea lor.

Înțelegerea Tipizării Structurale vs. Nominale

Înainte de a aprofunda tipurile branded, să clarificăm diferența dintre tipizarea structurală și cea nominală.

Tipizarea Structurală (Duck Typing)

Într-un sistem de tipuri structural, două tipuri sunt considerate compatibile dacă au aceeași structură (adică, aceleași proprietăți cu aceleași tipuri). TypeScript folosește tipizarea structurală. Luați în considerare acest exemplu:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid în TypeScript

console.log(vector.x); // Output: 10

Chiar dacă Point și Vector sunt declarate ca tipuri distincte, TypeScript permite atribuirea unui obiect Point unei variabile Vector deoarece au aceeași structură. Acest lucru poate fi convenabil, dar poate duce și la erori dacă trebuie să faceți distincția între tipuri logic diferite care se întâmplă să aibă aceeași formă. De exemplu, gândiți-vă la coordonatele pentru latitudine/longitudine care s-ar putea potrivi incidental cu coordonatele pixelilor de pe ecran.

Tipizarea Nominală

Într-un sistem de tipuri nominal, tipurile sunt considerate compatibile doar dacă au același nume. Chiar dacă două tipuri au aceeași structură, ele sunt tratate ca distincte dacă au nume diferite. Limbaje precum Java și C# folosesc tipizarea nominală.

Nevoia de Tipuri Branded

Tipizarea structurală a TypeScript poate fi problematică atunci când trebuie să vă asigurați că o valoare aparține unui tip specific, indiferent de structura sa. De exemplu, luați în considerare reprezentarea monedelor. Ați putea avea tipuri diferite pentru USD și EUR, dar ambele ar putea fi reprezentate ca numere. Fără un mecanism pentru a le distinge, ați putea efectua accidental operațiuni pe moneda greșită.

Tipurile branded abordează această problemă permițându-vă să creați tipuri distincte care sunt structural similare, dar tratate ca diferite de către sistemul de tipuri. Acest lucru sporește siguranța tipurilor și previne erorile care altfel ar putea trece neobservate.

Implementarea Tipurilor Branded în TypeScript

Tipurile branded sunt implementate folosind tipuri de intersecție și un simbol unic sau un literal de șir de caractere. Ideea este de a adăuga un "brand" unui tip care îl distinge de alte tipuri cu aceeași structură.

Folosirea Simbolurilor (Recomandat)

Folosirea simbolurilor pentru branding este în general preferată, deoarece simbolurile sunt garantate a fi unice.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Decomentarea liniei următoare va cauza o eroare de tip
// const invalidOperation = addUSD(usd1, eur1);

În acest exemplu, USD și EUR sunt tipuri branded bazate pe tipul number. unique symbol asigură că aceste tipuri sunt distincte. Funcțiile createUSD și createEUR sunt folosite pentru a crea valori de aceste tipuri, iar funcția addUSD acceptă doar valori USD. Încercarea de a adăuga o valoare EUR la o valoare USD va duce la o eroare de tip.

Folosirea Literalilor de Șir de Caractere

Puteți folosi și literali de șir de caractere pentru branding, deși această abordare este mai puțin robustă decât folosirea simbolurilor, deoarece literalii de șir de caractere nu sunt garantați a fi unici.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Decomentarea liniei următoare va cauza o eroare de tip
// const invalidOperation = addUSD(usd1, eur1);

Acest exemplu obține același rezultat ca și cel anterior, dar folosind literali de șir de caractere în loc de simboluri. Deși este mai simplu, este important să vă asigurați că literalii de șir de caractere folosiți pentru branding sunt unici în baza de cod.

Exemple Practice și Cazuri de Utilizare

Tipurile branded pot fi aplicate în diverse scenarii în care trebuie să impuneți siguranța tipurilor dincolo de compatibilitatea structurală.

ID-uri

Luați în considerare un sistem cu diferite tipuri de ID-uri, cum ar fi UserID, ProductID și OrderID. Toate aceste ID-uri ar putea fi reprezentate ca numere sau șiruri de caractere, dar doriți să preveniți amestecarea accidentală a diferitelor tipuri de ID-uri.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... preia datele utilizatorului
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... preia datele produsului
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("Utilizator:", user);
console.log("Produs:", product);

// Decomentarea liniei următoare va cauza o eroare de tip
// const invalidCall = getUser(productID);

Acest exemplu demonstrează cum tipurile branded pot preveni transmiterea unui ProductID către o funcție care se așteaptă la un UserID, sporind siguranța tipurilor.

Valori Specifice Domeniului

Tipurile branded pot fi, de asemenea, utile pentru a reprezenta valori specifice domeniului cu constrângeri. De exemplu, ați putea avea un tip pentru procente care ar trebui să fie întotdeauna între 0 și 100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Procentul trebuie să fie între 0 și 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Preț redus:", discountedPrice);

  // Decomentarea liniei următoare va cauza o eroare la rulare
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Acest exemplu arată cum se poate impune o constrângere asupra valorii unui tip branded în timpul execuției. Deși sistemul de tipuri nu poate garanta că o valoare Percentage este întotdeauna între 0 și 100, funcția createPercentage poate impune această constrângere în timpul execuției. Puteți folosi, de asemenea, biblioteci precum io-ts pentru a impune validarea la rulare a tipurilor branded.

Reprezentări de Dată și Timp

Lucrul cu datele și orele poate fi dificil din cauza diverselor formate și fusuri orare. Tipurile branded pot ajuta la diferențierea între diferite reprezentări de dată și timp.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Validează că șirul de dată este în format UTC (de ex., ISO 8601 cu Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Format de dată UTC invalid');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Validează că șirul de dată este în format de dată locală (de ex., YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Format de dată locală invalid');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Efectuează conversia fusului orar
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("Dată UTC:", utcDate);
  console.log("Dată Locală:", localDate);
} catch (error) {
  console.error(error);
}

Acest exemplu face diferența între datele UTC și cele locale, asigurându-vă că lucrați cu reprezentarea corectă a datei și orei în diferite părți ale aplicației. Validarea la rulare asigură că doar șirurile de date formatate corect pot fi atribuite acestor tipuri.

Bune Practici pentru Utilizarea Tipurilor Branded

Pentru a utiliza eficient tipurile branded în TypeScript, luați în considerare următoarele bune practici:

Avantajele Tipurilor Branded

Dezavantajele Tipurilor Branded

Alternative la Tipurile Branded

Deși tipurile branded sunt o tehnică puternică pentru a obține tipizare nominală în TypeScript, există abordări alternative pe care le-ați putea lua în considerare.

Tipuri Opacice (Opaque Types)

Tipurile opacice sunt similare cu tipurile branded, dar oferă o modalitate mai explicită de a ascunde tipul subiacent. TypeScript nu are suport nativ pentru tipuri opacice, dar le puteți simula folosind module și simboluri private.

Clase

Folosirea claselor poate oferi o abordare mai orientată pe obiecte pentru definirea tipurilor distincte. Deși clasele sunt tipizate structural în TypeScript, ele oferă o separare mai clară a responsabilităților și pot fi folosite pentru a impune constrângeri prin metode.

Biblioteci precum `io-ts` sau `zod`

Aceste biblioteci oferă validare sofisticată a tipurilor la rulare și pot fi combinate cu tipurile branded pentru a asigura siguranța atât la compilare, cât și la rulare.

Concluzie

Tipurile branded din TypeScript sunt un instrument valoros pentru sporirea siguranței tipurilor și a clarității codului într-un sistem de tipuri structural. Adăugând un "brand" unui tip, puteți impune tipizarea nominală și preveni amestecarea accidentală a tipurilor structural similare, dar logic diferite. Deși tipurile branded introduc o oarecare complexitate și overhead, beneficiile siguranței îmbunătățite a tipurilor și ale mentenabilității codului depășesc adesea dezavantajele. Luați în considerare utilizarea tipurilor branded în scenarii în care trebuie să vă asigurați că o valoare aparține unui tip specific, indiferent de structura sa.

Înțelegând principiile din spatele tipizării structurale și nominale și aplicând bunele practici prezentate în acest articol, puteți valorifica eficient tipurile branded pentru a scrie cod TypeScript mai robust și mai ușor de întreținut. De la reprezentarea monedelor și a ID-urilor până la impunerea constrângerilor specifice domeniului, tipurile branded oferă un mecanism flexibil și puternic pentru sporirea siguranței tipurilor în proiectele dumneavoastră.

Pe măsură ce lucrați cu TypeScript, explorați diversele tehnici și biblioteci disponibile pentru validarea și impunerea tipurilor. Luați în considerare utilizarea tipurilor branded în conjuncție cu biblioteci de validare la rulare precum io-ts sau zod pentru a obține o abordare cuprinzătoare a siguranței tipurilor.