Utforsk TypeScript branded types, en kraftig teknikk for å oppnå nominell typing i et strukturelt typesystem. Lær hvordan du kan forbedre typesikkerhet og kodeklarhet.
TypeScript Branded Types: Nominell Typing i et Strukturelt System
TypeScripts strukturelle typesystem tilbyr fleksibilitet, men kan noen ganger føre til uventet oppførsel. Branded types gir en måte å håndheve nominell typing på, noe som forbedrer typesikkerhet og kodeklarhet. Denne artikkelen utforsker branded types i detalj, og gir praktiske eksempler og beste praksis for implementeringen.
Forstå Strukturell vs. Nominell Typing
Før vi dykker inn i branded types, la oss klargjøre forskjellen mellom strukturell og nominell typing.
Strukturell Typing (Duck Typing)
I et strukturelt typesystem anses to typer som kompatible hvis de har samme struktur (dvs. de samme egenskapene med de samme typene). TypeScript bruker strukturell typing. Se på dette eksempelet:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Gyldig i TypeScript
console.log(vector.x); // Utdata: 10
Selv om Point
og Vector
er deklarert som distinkte typer, tillater TypeScript å tildele et Point
-objekt til en Vector
-variabel fordi de deler samme struktur. Dette kan være praktisk, men det kan også føre til feil hvis du trenger å skille mellom logisk forskjellige typer som tilfeldigvis har samme form. For eksempel, tenk på koordinater for breddegrad/lengdegrad som tilfeldigvis kan matche skjermpikselkoordinater.
Nominell Typing
I et nominelt typesystem anses typer som kompatible kun hvis de har samme navn. Selv om to typer har samme struktur, blir de behandlet som distinkte hvis de har forskjellige navn. Språk som Java og C# bruker nominell typing.
Behovet for Branded Types
TypeScripts strukturelle typing kan være problematisk når du trenger å sikre at en verdi tilhører en spesifikk type, uavhengig av dens struktur. For eksempel, tenk på representasjon av valutaer. Du kan ha forskjellige typer for USD og EUR, men begge kan representeres som tall. Uten en mekanisme for å skille dem, kan du ved et uhell utføre operasjoner på feil valuta.
Branded types løser dette problemet ved å la deg lage distinkte typer som er strukturelt like, men som behandles som forskjellige av typesystemet. Dette forbedrer typesikkerheten og forhindrer feil som ellers kunne ha sluppet gjennom.
Implementering av Branded Types i TypeScript
Branded types implementeres ved hjelp av intersection-typer og et unikt symbol eller en strengliteral. Ideen er å legge til et "merke" (brand) til en type som skiller den fra andre typer med samme struktur.
Bruke Symboler (Anbefalt)
Å bruke symboler for merking er generelt foretrukket fordi symboler er garantert å være unike.
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);
// Hvis du fjerner kommentaren på neste linje, vil det forårsake en typefeil
// const invalidOperation = addUSD(usd1, eur1);
I dette eksempelet er USD
og EUR
branded types basert på number
-typen. Det unike symbolet (unique symbol
) sikrer at disse typene er distinkte. Funksjonene createUSD
og createEUR
brukes til å lage verdier av disse typene, og addUSD
-funksjonen aksepterer bare USD
-verdier. Forsøk på å legge en EUR
-verdi til en USD
-verdi vil resultere i en typefeil.
Bruke Strengliteraler
Du kan også bruke strengliteraler for merking, selv om denne tilnærmingen er mindre robust enn å bruke symboler fordi strengliteraler ikke er garantert å være unike.
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);
// Hvis du fjerner kommentaren på neste linje, vil det forårsake en typefeil
// const invalidOperation = addUSD(usd1, eur1);
Dette eksempelet oppnår samme resultat som det forrige, men bruker strengliteraler i stedet for symboler. Selv om det er enklere, er det viktig å sikre at strengliteralene som brukes til merking er unike i kodebasen din.
Praktiske Eksempler og Bruksområder
Branded types kan brukes i ulike scenarioer der du trenger å håndheve typesikkerhet utover strukturell kompatibilitet.
ID-er
Tenk deg et system med forskjellige typer ID-er, som UserID
, ProductID
og OrderID
. Alle disse ID-ene kan representeres som tall eller strenger, men du vil forhindre utilsiktet blanding av forskjellige ID-typer.
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 } {
// ... hent brukerdata
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... hent produktdata
return { name: "Eksempelprodukt", 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);
// Hvis du fjerner kommentaren på neste linje, vil det forårsake en typefeil
// const invalidCall = getUser(productID);
Dette eksempelet demonstrerer hvordan branded types kan forhindre at en ProductID
sendes til en funksjon som forventer en UserID
, noe som forbedrer typesikkerheten.
Domene-spesifikke Verdier
Branded types kan også være nyttige for å representere domenespesifikke verdier med begrensninger. For eksempel kan du ha en type for prosenter som alltid skal være mellom 0 og 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);
// Hvis du fjerner kommentaren på neste linje, vil det forårsake en feil under kjøring
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Dette eksempelet viser hvordan man kan håndheve en begrensning på verdien av en branded type under kjøring (runtime). Selv om typesystemet ikke kan garantere at en Percentage
-verdi alltid er mellom 0 og 100, kan createPercentage
-funksjonen håndheve denne begrensningen under kjøring. Du kan også bruke biblioteker som io-ts for å håndheve runtime-validering av branded types.
Representasjoner av Dato og Tid
Å jobbe med datoer og tider kan være vanskelig på grunn av ulike formater og tidssoner. Branded types kan hjelpe med å skille mellom forskjellige representasjoner av dato og tid.
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 {
// Valider at datostrengen er i UTC-format (f.eks. ISO 8601 med 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 {
// Valider at datostrengen er i lokalt datoformat (f.eks. 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 {
// Utfør tidssonekonvertering
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);
}
Dette eksempelet skiller mellom UTC- og lokale datoer, og sikrer at du jobber med riktig dato- og tidsrepresentasjon i ulike deler av applikasjonen din. Runtime-validering sikrer at kun korrekt formaterte datostrenger kan tildeles disse typene.
Beste Praksis for Bruk av Branded Types
For å bruke branded types effektivt i TypeScript, bør du vurdere følgende beste praksis:
- Bruk Symboler for Merking: Symboler gir den sterkeste garantien for unikhet, noe som reduserer risikoen for typefeil.
- Lag Hjelpefunksjoner: Bruk hjelpefunksjoner for å lage verdier av branded types. Dette gir et sentralt punkt for validering og sikrer konsistens.
- Bruk Runtime-validering: Selv om branded types forbedrer typesikkerheten, forhindrer de ikke at feil verdier tildeles under kjøring. Bruk runtime-validering for å håndheve begrensninger.
- Dokumenter Branded Types: Dokumenter formålet og begrensningene for hver branded type tydelig for å forbedre vedlikeholdbarheten til koden.
- Vurder Ytelseskonsekvenser: Branded types introduserer en liten overhead på grunn av intersection-typen og behovet for hjelpefunksjoner. Vurder ytelsespåvirkningen i ytelseskritiske deler av koden din.
Fordeler med Branded Types
- Forbedret Typesikkerhet: Forhindrer utilsiktet blanding av strukturelt like, men logisk forskjellige typer.
- Forbedret Kodeklarhet: Gjør koden mer lesbar og lettere å forstå ved å eksplisitt skille mellom typer.
- Reduserte Feil: Fanger opp potensielle feil på kompileringstidspunktet, noe som reduserer risikoen for runtime-feil.
- Økt Vedlikeholdbarhet: Gjør koden enklere å vedlikeholde og refaktorere ved å gi en klar ansvarsfordeling.
Ulemper med Branded Types
- Økt Kompleksitet: Legger til kompleksitet i kodebasen, spesielt når man håndterer mange branded types.
- Runtime Overhead: Introduserer en liten runtime-overhead på grunn av behovet for hjelpefunksjoner og runtime-validering.
- Potensial for Boilerplate: Kan føre til boilerplate-kode, spesielt ved opprettelse og validering av branded types.
Alternativer til Branded Types
Selv om branded types er en kraftig teknikk for å oppnå nominell typing i TypeScript, finnes det alternative tilnærminger du kan vurdere.
Opaque Typer
Opaque typer ligner på branded types, men gir en mer eksplisitt måte å skjule den underliggende typen. TypeScript har ikke innebygd støtte for opaque typer, men du kan simulere dem ved hjelp av moduler og private symboler.
Klasser
Bruk av klasser kan gi en mer objektorientert tilnærming til å definere distinkte typer. Selv om klasser er strukturelt typet i TypeScript, tilbyr de en klarere ansvarsfordeling og kan brukes til å håndheve begrensninger gjennom metoder.
Biblioteker som io-ts
eller zod
Disse bibliotekene tilbyr sofistikert runtime-typevalidering og kan kombineres med branded types for å sikre både kompileringstids- og kjøretidssikkerhet.
Konklusjon
TypeScript branded types er et verdifullt verktøy for å forbedre typesikkerhet og kodeklarhet i et strukturelt typesystem. Ved å legge til et "merke" (brand) til en type, kan du håndheve nominell typing og forhindre utilsiktet blanding av strukturelt like, men logisk forskjellige typer. Selv om branded types introduserer noe kompleksitet og overhead, veier fordelene med forbedret typesikkerhet og vedlikeholdbarhet ofte opp for ulempene. Vurder å bruke branded types i scenarioer der du trenger å sikre at en verdi tilhører en spesifikk type, uavhengig av dens struktur.
Ved å forstå prinsippene bak strukturell og nominell typing, og ved å anvende beste praksis som er beskrevet i denne artikkelen, kan du effektivt utnytte branded types for å skrive mer robust og vedlikeholdbar TypeScript-kode. Fra å representere valutaer og ID-er til å håndheve domenespesifikke begrensninger, gir branded types en fleksibel og kraftig mekanisme for å forbedre typesikkerheten i prosjektene dine.
Når du jobber med TypeScript, utforsk de ulike teknikkene og bibliotekene som er tilgjengelige for typevalidering og -håndhevelse. Vurder å bruke branded types i kombinasjon med runtime-valideringsbiblioteker som io-ts
eller zod
for å oppnå en helhetlig tilnærming til typesikkerhet.