Suomi

Tutustu TypeScriptin brändättyihin tyyppeihin, tehokkaaseen tekniikkaan nominaalisen tyypityksen saavuttamiseksi rakenteellisessa tyyppijärjestelmässä. Opi parantamaan tyyppiturvallisuutta ja koodin selkeyttä.

TypeScriptin brändätyt tyypit: Nominaalinen tyypitys rakenteellisessa järjestelmässä

TypeScriptin rakenteellinen tyyppijärjestelmä tarjoaa joustavuutta, mutta voi joskus johtaa odottamattomaan käytökseen. Brändätyt tyypit tarjoavat tavan pakottaa nominaalinen tyypitys, mikä parantaa tyyppiturvallisuutta ja koodin selkeyttä. Tämä artikkeli tutkii brändättyjä tyyppejä yksityiskohtaisesti, tarjoten käytännön esimerkkejä ja parhaita käytäntöjä niiden toteuttamiseen.

Rakenteellisen ja nominaalisen tyypityksen ymmärtäminen

Ennen kuin syvennymme brändättyihin tyyppeihin, selvitetään ero rakenteellisen ja nominaalisen tyypityksen välillä.

Rakenteellinen tyypitys (Duck Typing)

Rakenteellisessa tyyppijärjestelmässä kaksi tyyppiä katsotaan yhteensopiviksi, jos niillä on sama rakenne (ts. samat ominaisuudet samoilla tyypeillä). TypeScript käyttää rakenteellista tyypitystä. Tarkastellaan tätä esimerkkiä:


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

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

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Sallittu TypeScriptissä

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

Vaikka Point ja Vector on määritelty erillisinä tyyppeinä, TypeScript sallii Point-olion sijoittamisen Vector-muuttujaan, koska niillä on sama rakenne. Tämä voi olla kätevää, mutta se voi myös johtaa virheisiin, jos sinun on erotettava loogisesti erilaiset tyypit, joilla sattuu olemaan sama muoto. Esimerkkinä leveys- ja pituusasteiden koordinaatit, jotka saattavat sattumalta vastata näytön pikselikoordinaatteja.

Nominaalinen tyypitys

Nominaalisessa tyyppijärjestelmässä tyypit katsotaan yhteensopiviksi vain, jos niillä on sama nimi. Vaikka kahdella tyypillä olisi sama rakenne, niitä pidetään erillisinä, jos niillä on eri nimet. Kielet kuten Java ja C# käyttävät nominaalista tyypitystä.

Brändättyjen tyyppien tarve

TypeScriptin rakenteellinen tyypitys voi olla ongelmallinen, kun sinun on varmistettava, että arvo kuuluu tiettyyn tyyppiin sen rakenteesta riippumatta. Esimerkiksi valuuttojen esittäminen. Sinulla voi olla eri tyypit USD:lle ja EUR:lle, mutta molemmat voidaan esittää numeroina. Ilman mekanismia niiden erottamiseen, voisit vahingossa suorittaa operaatioita väärällä valuutalla.

Brändätyt tyypit ratkaisevat tämän ongelman antamalla sinun luoda erillisiä tyyppejä, jotka ovat rakenteellisesti samanlaisia, mutta joita tyyppijärjestelmä käsittelee eri tavoin. Tämä parantaa tyyppiturvallisuutta ja estää virheitä, jotka muuten saattaisivat livahtaa läpi.

Brändättyjen tyyppien toteuttaminen TypeScriptissä

Brändätyt tyypit toteutetaan käyttämällä risteystyyppejä ja uniikkia symbolia tai merkkijonoliteraalia. Ajatuksena on lisätä tyyppiin "brändi", joka erottaa sen muista samankaltaisen rakenteen omaavista tyypeistä.

Symbolien käyttö (suositeltu)

Symbolien käyttö brändäykseen on yleensä suositeltavampaa, koska symbolit ovat taatusti uniikkeja.


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

// Seuraavan rivin kommentoinnin poistaminen aiheuttaa tyyppivirheen
// const invalidOperation = addUSD(usd1, eur1);

Tässä esimerkissä USD ja EUR ovat number-tyyppiin perustuvia brändättyjä tyyppejä. unique symbol varmistaa, että nämä tyypit ovat erillisiä. Funktioita createUSD ja createEUR käytetään näiden tyyppien arvojen luomiseen, ja addUSD-funktio hyväksyy vain USD-arvoja. Yritys lisätä EUR-arvo USD-arvoon johtaa tyyppivirheeseen.

Merkkijonoliteraalien käyttö

Voit myös käyttää merkkijonoliteraaleja brändäykseen, vaikka tämä lähestymistapa on vähemmän vankka kuin symbolien käyttö, koska merkkijonoliteraalit eivät ole taatusti uniikkeja.


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

// Seuraavan rivin kommentoinnin poistaminen aiheuttaa tyyppivirheen
// const invalidOperation = addUSD(usd1, eur1);

Tämä esimerkki saavuttaa saman tuloksen kuin edellinen, mutta käyttää symboleiden sijaan merkkijonoliteraaleja. Vaikka tämä on yksinkertaisempaa, on tärkeää varmistaa, että brändäykseen käytetyt merkkijonoliteraalit ovat uniikkeja koodikannassasi.

Käytännön esimerkkejä ja käyttötapauksia

Brändättyjä tyyppejä voidaan soveltaa moniin skenaarioihin, joissa sinun on pakotettava tyyppiturvallisuus rakenteellisen yhteensopivuuden lisäksi.

Tunnisteet (ID)

Harkitse järjestelmää, jossa on erityyppisiä tunnisteita, kuten UserID, ProductID ja OrderID. Kaikki nämä tunnisteet voivat olla numeroita tai merkkijonoja, mutta haluat estää erityyppisten tunnisteiden sekoittumisen vahingossa.


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 } {
  // ... hae käyttäjän tiedot
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... hae tuotteen tiedot
  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);

// Seuraavan rivin kommentoinnin poistaminen aiheuttaa tyyppivirheen
// const invalidCall = getUser(productID);

Tämä esimerkki osoittaa, kuinka brändätyt tyypit voivat estää ProductID-tunnisteen välittämisen funktiolle, joka odottaa UserID-tunnistetta, mikä parantaa tyyppiturvallisuutta.

Toimialuekohtaiset arvot

Brändätyt tyypit voivat olla hyödyllisiä myös toimialuekohtaisten, rajoituksia sisältävien arvojen esittämiseen. Esimerkiksi sinulla voi olla tyyppi prosenttiluvuille, joiden tulisi aina olla välillä 0 ja 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('Prosenttiluvun on oltava 0 ja 100 välillä');
  }
  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);

  // Seuraavan rivin kommentoinnin poistaminen aiheuttaa ajonaikaisen virheen
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Tämä esimerkki näyttää, kuinka brändätyn tyypin arvolle voidaan asettaa rajoitus ajon aikana. Vaikka tyyppijärjestelmä ei voi taata, että Percentage-arvo on aina 0 ja 100 välillä, createPercentage-funktio voi valvoa tätä rajoitusta ajon aikana. Voit myös käyttää kirjastoja, kuten io-ts, brändättyjen tyyppien ajonaikaiseen validointiin.

Päivämäärän ja ajan esitysmuodot

Päivämäärien ja aikojen käsittely voi olla hankalaa erilaisten muotojen ja aikavyöhykkeiden vuoksi. Brändätyt tyypit voivat auttaa erottamaan erilaiset päivämäärän ja ajan esitysmuodot toisistaan.


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 {
  // Varmista, että päiväysmerkkijono on UTC-muodossa (esim. ISO 8601 Z-kirjaimella)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Virheellinen UTC-päivämäärän muoto');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Varmista, että päiväysmerkkijono on paikallisessa päivämäärämuodossa (esim. VVVV-KK-PP)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Virheellinen paikallisen päivämäärän muoto');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Suorita aikavyöhykemuunnos
  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);
}

Tämä esimerkki erottaa UTC- ja paikalliset päivämäärät toisistaan, varmistaen, että työskentelet oikean päivämäärän ja ajan esitysmuodon kanssa sovelluksesi eri osissa. Ajonaikainen validointi varmistaa, että vain oikein muotoillut päiväysmerkkijonot voidaan sijoittaa näihin tyyppeihin.

Parhaat käytännöt brändättyjen tyyppien käyttöön

Jotta voit käyttää brändättyjä tyyppejä tehokkaasti TypeScriptissä, harkitse seuraavia parhaita käytäntöjä:

Brändättyjen tyyppien edut

Brändättyjen tyyppien haitat

Vaihtoehtoja brändätyille tyypeille

Vaikka brändätyt tyypit ovat tehokas tekniikka nominaalisen tyypityksen saavuttamiseksi TypeScriptissä, on olemassa vaihtoehtoisia lähestymistapoja, joita voit harkita.

Läpinäkymättömät tyypit (Opaque Types)

Läpinäkymättömät tyypit ovat samankaltaisia kuin brändätyt tyypit, mutta ne tarjoavat selkeämmän tavan piilottaa pohjalla oleva tyyppi. TypeScriptissä ei ole sisäänrakennettua tukea läpinäkymättömille tyypeille, mutta voit simuloida niitä moduulien ja yksityisten symbolien avulla.

Luokat

Luokkien käyttö voi tarjota olio-orientoituneemman lähestymistavan erillisten tyyppien määrittelyyn. Vaikka luokat ovat rakenteellisesti tyypitettyjä TypeScriptissä, ne tarjoavat selkeämmän vastuunjaon ja niitä voidaan käyttää rajoitusten valvomiseen metodien avulla.

Kirjastot kuten `io-ts` tai `zod`

Nämä kirjastot tarjoavat kehittyneen ajonaikaisen tyyppivalidoinnin ja ne voidaan yhdistää brändättyihin tyyppeihin sekä käännösaikaisen että ajonaikaisen turvallisuuden varmistamiseksi.

Yhteenveto

TypeScriptin brändätyt tyypit ovat arvokas työkalu tyyppiturvallisuuden ja koodin selkeyden parantamiseen rakenteellisessa tyyppijärjestelmässä. Lisäämällä tyyppiin "brändin" voit pakottaa nominaalisen tyypityksen ja estää rakenteellisesti samankaltaisten mutta loogisesti erilaisten tyyppien tahattoman sekoittumisen. Vaikka brändätyt tyypit tuovat jonkin verran monimutkaisuutta ja yleiskustannuksia, parantuneen tyyppiturvallisuuden ja koodin ylläpidettävyyden edut usein ylittävät haitat. Harkitse brändättyjen tyyppien käyttöä skenaarioissa, joissa sinun on varmistettava, että arvo kuuluu tiettyyn tyyppiin sen rakenteesta riippumatta.

Ymmärtämällä rakenteellisen ja nominaalisen tyypityksen periaatteet ja soveltamalla tässä artikkelissa esitettyjä parhaita käytäntöjä voit tehokkaasti hyödyntää brändättyjä tyyppejä kirjoittaaksesi vankempaa ja ylläpidettävämpää TypeScript-koodia. Valuuttojen ja tunnisteiden esittämisestä toimialuekohtaisten rajoitusten valvontaan brändätyt tyypit tarjoavat joustavan ja tehokkaan mekanismin tyyppiturvallisuuden parantamiseen projekteissasi.

Kun työskentelet TypeScriptin parissa, tutustu erilaisiin saatavilla oleviin tekniikoihin ja kirjastoihin tyyppien validointia ja valvontaa varten. Harkitse brändättyjen tyyppien käyttöä yhdessä ajonaikaisten validointikirjastojen, kuten io-ts tai zod, kanssa saavuttaaksesi kattavan lähestymistavan tyyppiturvallisuuteen.