Dansk

Udforsk TypeScript branded types, en kraftfuld teknik til at opnå nominel typning i et strukturelt typesystem. Lær hvordan du forbedrer typesikkerhed og kodens klarhed.

TypeScript Branded Types: Nominel Typning i et Strukturelt System

TypeScript's strukturelle typesystem tilbyder fleksibilitet, men kan nogle gange føre til uventet adfærd. Branded types giver en måde at håndhæve nominel typning på, hvilket forbedrer typesikkerhed og kodens klarhed. Denne artikel udforsker branded types i detaljer og giver praktiske eksempler og bedste praksis for deres implementering.

Forståelse af Strukturel vs. Nominel Typning

Før vi dykker ned i branded types, lad os afklare forskellen mellem strukturel og nominel typning.

Strukturel Typning (Duck Typing)

I et strukturelt typesystem betragtes to typer som kompatible, hvis de har den samme struktur (dvs. de samme egenskaber med de samme typer). TypeScript bruger strukturel typning. Overvej dette eksempel:


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); // Output: 10

Selvom Point og Vector er erklæret som forskellige typer, tillader TypeScript at tildele et Point-objekt til en Vector-variabel, fordi de deler den samme struktur. Dette kan være praktisk, men det kan også føre til fejl, hvis man har brug for at skelne mellem logisk forskellige typer, der tilfældigvis har den samme form. For eksempel, tænk på koordinater for bredde/længdegrad, der tilfældigvis kan matche skærmens pixelkoordinater.

Nominel Typning

I et nominelt typesystem betragtes typer kun som kompatible, hvis de har det samme navn. Selv hvis to typer har den samme struktur, behandles de som forskellige, hvis de har forskellige navne. Sprog som Java og C# bruger nominel typning.

Behovet for Branded Types

TypeScript's strukturelle typning kan være problematisk, når du skal sikre, at en værdi tilhører en specifik type, uanset dens struktur. For eksempel, overvej at repræsentere valutaer. Du kan have forskellige typer for USD og EUR, men de kunne begge repræsenteres som tal. Uden en mekanisme til at skelne dem, kunne du ved et uheld udføre operationer på den forkerte valuta.

Branded types løser dette problem ved at give dig mulighed for at skabe distinkte typer, der er strukturelt ens, men behandles som forskellige af typesystemet. Dette forbedrer typesikkerheden og forhindrer fejl, der ellers kunne slippe igennem.

Implementering af Branded Types i TypeScript

Branded types implementeres ved hjælp af intersection-typer og et unikt symbol eller en strengliteral. Ideen er at tilføje et "brand" til en type, der adskiller den fra andre typer med samme struktur.

Brug af Symboler (Anbefales)

Brug af symboler til branding foretrækkes generelt, fordi symboler er garanteret at være unikke.


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

// Fjernelse af kommentaren på næste linje vil forårsage en typefejl
// const invalidOperation = addUSD(usd1, eur1);

I dette eksempel er USD og EUR branded types baseret på number-typen. unique symbol sikrer, at disse typer er distinkte. Funktionerne createUSD og createEUR bruges til at oprette værdier af disse typer, og addUSD-funktionen accepterer kun USD-værdier. Forsøg på at tilføje en EUR-værdi til en USD-værdi vil resultere i en typefejl.

Brug af Strengliteraler

Du kan også bruge strengliteraler til branding, selvom denne tilgang er mindre robust end at bruge symboler, fordi strengliteraler ikke er garanteret at være unikke.


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

// Fjernelse af kommentaren på næste linje vil forårsage en typefejl
// const invalidOperation = addUSD(usd1, eur1);

Dette eksempel opnår det samme resultat som det forrige, men bruger strengliteraler i stedet for symboler. Selvom det er enklere, er det vigtigt at sikre, at de strengliteraler, der bruges til branding, er unikke i din kodebase.

Praktiske Eksempler og Anvendelsestilfælde

Branded types kan anvendes i forskellige scenarier, hvor du skal håndhæve typesikkerhed ud over strukturel kompatibilitet.

ID'er

Overvej et system med forskellige typer af ID'er, såsom UserID, ProductID og OrderID. Alle disse ID'er kan være repræsenteret som tal eller strenge, men du vil forhindre utilsigtet sammenblanding af forskellige 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 brugerdata
  return { name: "Alice" };
}

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

// Fjernelse af kommentaren på næste linje vil forårsage en typefejl
// const invalidCall = getUser(productID);

Dette eksempel demonstrerer, hvordan branded types kan forhindre, at et ProductID sendes til en funktion, der forventer et UserID, hvilket forbedrer typesikkerheden.

Domænespecifikke Værdier

Branded types kan også være nyttige til at repræsentere domænespecifikke værdier med begrænsninger. For eksempel kan du have en type for procenter, der altid skal være mellem 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('Procentdelen skal være mellem 0 og 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("Nedsat pris:", discountedPrice);

  // Fjernelse af kommentaren på næste linje vil forårsage en fejl under kørsel
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Dette eksempel viser, hvordan man håndhæver en begrænsning på værdien af en branded type under kørsel (runtime). Selvom typesystemet ikke kan garantere, at en Percentage-værdi altid er mellem 0 og 100, kan createPercentage-funktionen håndhæve denne begrænsning under kørsel. Du kan også bruge biblioteker som io-ts til at håndhæve runtime-validering af branded types.

Dato- og Tidsrepræsentationer

Arbejde med datoer og tider kan være vanskeligt på grund af forskellige formater og tidszoner. Branded types kan hjælpe med at skelne mellem forskellige dato- og tidsrepræsentationer.


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('Ugyldigt UTC-datoformat');
  }
  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('Ugyldigt lokalt datoformat');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Udfør tidszonekonvertering
  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 Dato:", utcDate);
  console.log("Lokal Dato:", localDate);
} catch (error) {
  console.error(error);
}

Dette eksempel skelner mellem UTC og lokale datoer, hvilket sikrer, at du arbejder med den korrekte dato- og tidsrepræsentation i forskellige dele af din applikation. Runtime-validering sikrer, at kun korrekt formaterede datostrenge kan tildeles disse typer.

Bedste Praksis for Brug af Branded Types

For at bruge branded types effektivt i TypeScript, overvej følgende bedste praksis:

Fordele ved Branded Types

Ulemper ved Branded Types

Alternativer til Branded Types

Selvom branded types er en kraftfuld teknik til at opnå nominel typning i TypeScript, findes der alternative tilgange, som du kan overveje.

Opaque Types

Opaque types ligner branded types, men giver en mere eksplicit måde at skjule den underliggende type på. TypeScript har ikke indbygget understøttelse af opaque types, men du kan simulere dem ved hjælp af moduler og private symboler.

Klasser

Brug af klasser kan give en mere objektorienteret tilgang til at definere distinkte typer. Selvom klasser er strukturelt typede i TypeScript, tilbyder de en klarere adskillelse af ansvarsområder og kan bruges til at håndhæve begrænsninger gennem metoder.

Biblioteker som `io-ts` eller `zod`

Disse biblioteker tilbyder sofistikeret runtime-typevalidering og kan kombineres med branded types for at sikre både compile-time og runtime sikkerhed.

Konklusion

TypeScript branded types er et værdifuldt værktøj til at forbedre typesikkerhed og kodens klarhed i et strukturelt typesystem. Ved at tilføje et "brand" til en type kan du håndhæve nominel typning og forhindre utilsigtet sammenblanding af strukturelt ens, men logisk forskellige typer. Selvom branded types introducerer en vis kompleksitet og overhead, opvejer fordelene ved forbedret typesikkerhed og kodens vedligeholdelighed ofte ulemperne. Overvej at bruge branded types i scenarier, hvor du skal sikre, at en værdi tilhører en specifik type, uanset dens struktur.

Ved at forstå principperne bag strukturel og nominel typning og ved at anvende de bedste praksisser, der er beskrevet i denne artikel, kan du effektivt udnytte branded types til at skrive mere robust og vedligeholdelig TypeScript-kode. Fra at repræsentere valutaer og ID'er til at håndhæve domænespecifikke begrænsninger, giver branded types en fleksibel og kraftfuld mekanisme til at forbedre typesikkerheden i dine projekter.

Når du arbejder med TypeScript, kan du udforske de forskellige teknikker og biblioteker, der er tilgængelige for typevalidering og -håndhævelse. Overvej at bruge branded types i kombination med runtime-valideringsbiblioteker som io-ts eller zod for at opnå en omfattende tilgang til typesikkerhed.