Eesti

Avastage TypeScripti branded-tüübid – võimas tehnika nominaalse tüüpimise saavutamiseks struktuurses süsteemis. Õppige parandama tüübiohutust ja koodi selgust.

TypeScripti Branded-tüübid: Nominaalne tüüpimine struktuurses süsteemis

TypeScripti struktuurne tüübisüsteem pakub paindlikkust, kuid võib mõnikord põhjustada ootamatut käitumist. Branded-tüübid pakuvad võimalust rakendada nominaalset tüüpimist, parandades tüübiohutust ja koodi selgust. See artikkel uurib branded-tüüpe üksikasjalikult, pakkudes praktilisi näiteid ja parimaid praktikaid nende rakendamiseks.

Struktuurse vs. nominaalse tüüpimise mõistmine

Enne branded-tüüpidesse süvenemist selgitame struktuurse ja nominaalse tüüpimise erinevust.

Struktuurne tüüpimine (Duck Typing)

Struktuurses tüübisüsteemis peetakse kahte tüüpi ühilduvaks, kui neil on sama struktuur (st samad omadused samade tüüpidega). TypeScript kasutab struktuurset tüüpimist. Vaatleme seda näidet:


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

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

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

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

Kuigi Point ja Vector on deklareeritud eraldi tüüpidena, lubab TypeScript määrata Point-objekti Vector-muutujale, kuna neil on sama struktuur. See võib olla mugav, kuid võib põhjustada ka vigu, kui on vaja eristada loogiliselt erinevaid tüüpe, millel on juhtumisi sama kuju. Näiteks laius- ja pikkuskraadide koordinaadid, mis võivad juhuslikult kattuda ekraani pikslite koordinaatidega.

Nominaalne tüüpimine

Nominaalses tüübisüsteemis peetakse tüüpe ühilduvaks ainult siis, kui neil on sama nimi. Isegi kui kahel tüübil on sama struktuur, käsitletakse neid eraldiseisvatena, kui neil on erinevad nimed. Keeled nagu Java ja C# kasutavad nominaalset tüüpimist.

Vajadus Branded-tüüpide järele

TypeScripti struktuurne tüüpimine võib olla problemaatiline, kui on vaja tagada, et väärtus kuulub kindlasse tüüpi, olenemata selle struktuurist. Näiteks valuutade esitamine. Teil võivad olla erinevad tüübid USD ja EUR jaoks, kuid mõlemad võiksid olla esitatud numbritena. Ilma mehhanismita nende eristamiseks võiksite kogemata teha tehteid vale valuutaga.

Branded-tüübid lahendavad selle probleemi, võimaldades luua eristatavaid tüüpe, mis on struktuurilt sarnased, kuid mida tüübisüsteem käsitleb erinevatena. See parandab tüübiohutust ja ennetab vigu, mis muidu võiksid läbi lipsata.

Branded-tüüpide rakendamine TypeScriptis

Branded-tüübid rakendatakse, kasutades ühisosa tüüpe (intersection types) ja unikaalset sümbolit või stringiliteraali. Idee on lisada tüübile "bränd", mis eristab seda teistest sama struktuuriga tüüpidest.

Sümbolite kasutamine (soovitatav)

Sümbolite kasutamine brändimiseks on üldiselt eelistatud, kuna sümbolid on garanteeritult unikaalsed.


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);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

Selles näites on USD ja EUR branded-tüübid, mis põhinevad number-tüübil. unique symbol tagab, et need tüübid on eristatavad. Funktsioone createUSD ja createEUR kasutatakse nende tüüpide väärtuste loomiseks ning funktsioon addUSD aktsepteerib ainult USD väärtusi. Katse liita EUR väärtus USD väärtusele põhjustab tüübivea.

Stringiliteraalide kasutamine

Brändimiseks saab kasutada ka stringiliteraale, kuigi see lähenemine on vähem robustne kui sümbolite kasutamine, kuna stringiliteraalid ei ole garanteeritult unikaalsed.


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);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

See näide saavutab sama tulemuse kui eelmine, kuid kasutab sümbolite asemel stringiliteraale. Kuigi see on lihtsam, on oluline tagada, et brändimiseks kasutatavad stringiliteraalid oleksid teie koodibaasis unikaalsed.

Praktilised näited ja kasutusjuhud

Branded-tüüpe saab rakendada erinevates stsenaariumides, kus on vaja tagada tüübiohutus väljaspool struktuurset ühilduvust.

ID-d

Kujutage ette süsteemi, kus on erinevat tüüpi ID-d, näiteks UserID, ProductID ja OrderID. Kõik need ID-d võivad olla esitatud numbrite või stringidena, kuid te soovite vältida erinevat tüüpi ID-de juhuslikku segamist.


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 } {
  // ... fetch user data
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... fetch product data
  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("User:", user);
console.log("Product:", product);

// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);

See näide demonstreerib, kuidas branded-tüübid võivad takistada ProductID edastamist funktsioonile, mis ootab UserID-d, parandades seeläbi tüübiohutust.

Domeenispetsiifilised väärtused

Branded-tüübid võivad olla kasulikud ka domeenispetsiifiliste piirangutega väärtuste esitamiseks. Näiteks võib teil olla tüüp protsentide jaoks, mis peaksid alati olema vahemikus 0 kuni 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('Percentage must be between 0 and 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("Discounted Price:", discountedPrice);

  // Uncommenting the next line will cause an error during runtime
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

See näide näitab, kuidas rakendada branded-tüübi väärtusele piirangut käitusajal. Kuigi tüübisüsteem ei saa garanteerida, et Percentage väärtus on alati vahemikus 0 kuni 100, saab createPercentage funktsioon selle piirangu käitusajal kehtestada. Branded-tüüpide käitusaegseks valideerimiseks võite kasutada ka teeke nagu io-ts.

Kuupäeva ja kellaaja esitused

Kuupäevade ja kellaaegadega töötamine võib olla keeruline erinevate formaatide ja ajavööndite tõttu. Branded-tüübid aitavad eristada erinevaid kuupäeva ja kellaaja esitusi.


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 {
  // Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Validate that the date string is in local date format (e.g., YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Perform time zone conversion
  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("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

See näide eristab UTC ja kohalikke kuupäevi, tagades, et töötate oma rakenduse erinevates osades õige kuupäeva ja kellaaja esitusega. Käitusaegne valideerimine tagab, et neile tüüpidele saab määrata ainult õigesti vormindatud kuupäevastringe.

Branded-tüüpide kasutamise parimad praktikad

Branded-tüüpide tõhusaks kasutamiseks TypeScriptis kaaluge järgmisi parimaid praktikaid:

Branded-tüüpide eelised

Branded-tüüpide puudused

Alternatiivid Branded-tüüpidele

Kuigi branded-tüübid on võimas tehnika nominaalse tüüpimise saavutamiseks TypeScriptis, on ka alternatiivseid lähenemisviise, mida võiksite kaaluda.

Läbipaistmatud tüübid (Opaque Types)

Läbipaistmatud tüübid on sarnased branded-tüüpidele, kuid pakuvad selgesõnalisemat viisi aluseks oleva tüübi peitmiseks. TypeScriptil ei ole sisseehitatud tuge läbipaistmatutele tüüpidele, kuid neid saab simuleerida moodulite ja privaatsete sümbolite abil.

Klassid

Klasside kasutamine võib pakkuda objektorienteeritumat lähenemist eristatavate tüüpide defineerimisel. Kuigi klassid on TypeScriptis struktuurselt tüübitud, pakuvad nad selgemat ülesannete eraldamist ja neid saab kasutada piirangute jõustamiseks meetodite kaudu.

Teegid nagu `io-ts` või `zod`

Need teegid pakuvad keerukat käitusaegset tüübivalideerimist ja neid saab kombineerida branded-tüüpidega, et tagada nii kompileerimisaegne kui ka käitusaegne ohutus.

Kokkuvõte

TypeScripti branded-tüübid on väärtuslik tööriist tüübiohutuse ja koodi selguse parandamiseks struktuurses tüübisüsteemis. Lisades tüübile "brändi", saate rakendada nominaalset tüüpimist ja vältida struktuurilt sarnaste, kuid loogiliselt erinevate tüüpide juhuslikku segamist. Kuigi branded-tüübid lisavad mõningast keerukust ja lisakoormust, kaaluvad parema tüübiohutuse ja koodi hooldatavuse eelised sageli puudused üles. Kaaluge branded-tüüpide kasutamist stsenaariumides, kus peate tagama, et väärtus kuulub kindlasse tüüpi, olenemata selle struktuurist.

Mõistes struktuurse ja nominaalse tüüpimise põhimõtteid ning rakendades selles artiklis kirjeldatud parimaid praktikaid, saate branded-tüüpe tõhusalt kasutada, et kirjutada robustsemat ja hooldatavamat TypeScripti koodi. Alates valuutade ja ID-de esitamisest kuni domeenispetsiifiliste piirangute jõustamiseni pakuvad branded-tüübid paindlikku ja võimsat mehhanismi tüübiohutuse parandamiseks teie projektides.

TypeScriptiga töötades uurige erinevaid tehnikaid ja teeke, mis on saadaval tüübivalideerimiseks ja jõustamiseks. Kaaluge branded-tüüpide kasutamist koos käitusaegse valideerimise teekidega nagu io-ts või zod, et saavutada terviklik lähenemine tüübiohutusele.