Alt ser riktig ut i koden – ingen feilmeldinger, ingen røde streker – men likevel havner husnummeret i feltet for postnummer. Med branded types i TypeScript kan du unngå slike feil, samtidig som typene får en klarere og mer detaljert betydning.
Har du noen gang stått midt i et flyttekaos og lett etter kaffetrakteren? Du vet den er et sted, men alle eskene ser like ut, og du må åpne frustrerende mange esker før kaffetrakteren dukker opp. I det øyeblikket angrer du kanskje litt på at du ikke merket eskene med "Kjøkkenutstyr", "Bøker" eller "Verktøy". Det hadde spart deg for tid, irritasjon og unødvendig leting.
Branded types i TypeScript fungerer litt på samme måte som merking av flytteesker: de gir typene en tydelig merkelapp som forteller hva de faktisk representerer. Det kan gjøre koden mer selvforklarende og mindre utsatt for blanding mellom typer – litt som tydelig merking av flytteesker sparer deg for tid og leting når du pakker ut.
Før vi skal forklare hva branded types er skal du få se på et av problemene som Branded types løser. I et eksempel hentet fra helsedomenet skal vi hente ut journaldata for en gitt pasient.
const getJournal = async (
patientId: string,
journalId: string,
) => await fetch(`api/patient/${patientId}/journal/${journalId}`)
const patientId = '12345'
const journalId = '67890'
await getJournal(journalId, patientId)
La du merke til en kritisk feil i koden? Feilen er at vi sender inn argumentene journalId
og patientId
i feil rekkefølge. Likevel kompilerer koden fint, siden begge parameterne er av typen string
. Å bruke branded types kan blant annet hjelpe oss å unngå feil som oppstår når posisjonelle argumenter havner i feil rekkefølge.
Hva er egentlig en branded type?
Branded types er en teknikk i TypeScript hvor vi "merker", eller "brander" typer med ekstra informasjon for å beskrive typen enda mer detaljert. Denne merkingen gjør at kompilatoren kan skille mellom variabler av samme underliggende type, men med forskjellig semantisk betydning.
Branded types støttes ikke offisielt av TypeScript, men en vanlig implementasjon kan se ut som dette:
declare const __brand: unique symbol
export type Brand<T, TBrand> = T & { [__brand]: TBrand }
Her brukes unique symbol
for å sørge for at man ikke kan lage brands som kolliderer. Den opprinnelige TypeScript typen sendes her inn som generisk argument T
, og så legger vi på attributtet __brand
, som i tillegg gir typen et nytt "merke".
La oss se om vi kan fikse eksempelet over ved å lage branded typer for journalId
og patientId
:
type PatientId = Brand<string, 'PatientId'>
type JournalId = Brand<string, 'JournalId'>
Vi bruker Brand-typen til å definere PatientId
og JournalId
, begge basert på string
, samtidig som TypeScript kan validere at vi bruker riktig type på riktig sted. La oss oppdatere funksjonen som henter journaldata med de nye typene:
const getJournal = async (
patientId: PatientId,
journalId: JournalId,
) => await fetch(`api/patient/${patientId}/journal/${journalId}`)
const patientId = '12345' as PatientId
const journalId = '67890' as JournalId
// Type error: Argument of type 'JournalId' is not assignable to parameter of type 'PatientId'
await getJournal(journalId, patientId)
Vi får nå følgende feilmelding fra TypeScript: Argument of type 'JournalId' is not assignable to parameter of type 'PatientId'
- supert!
Hvordan får en variabel et brand?
I eksempelet over ga vi patientId
et brand ved å bruke TypeScripts as
-operator for å tvinge typen til en branded type. Dette er den enkleste metoden, men også den mest sårbare for feil, siden du manuelt kan “brande” en verdi som egentlig ikke oppfyller kravene. I praksis vil du nok sette brand på typer hver gang du får data inn i applikasjonen din, f.eks fra et skjemafelt eller API. Selv om det er mulig å gi feil brand, så vil du gi brand til typer mye færre ganger enn de gangene du konsumerer den brandede typen, og totalt sett vil du få en applikasjon som gjør det vanskeligere å bruke typer feil.
For å gjøre branding tryggere kan du bruke type guards eller brand functions. Disse kombinerer gjerne runtime-sjekker med branding, slik at verdien kun får brand dersom den faktisk er gyldig.
Når string og number ikke sier nok - gi dem en mening
Et annet eksempel er håndtering av datoer. I applikasjoner viser vi gjerne samme dato i mange ulike formater, f.eks kan 5. januar 2025 representeres som 05.01.2025
, 5. jan. 25
eller ISO 8601 format 2025-01-05T12:00:00Z
. Alle disse bygger på den underliggende typen string
, men ved å lage en branded type for hver av dem øker vi lesbarheten og unngår å blande dem når vi for eksempel skal sortere eller filtrere datoer.
type NumericDate = Brand<string, 'NumericDate'>
type AlphanumericDate = Brand<string, 'AlphanumericDate'>
type ISODateString = Brand<string, 'ISODateString'>
const renderNumericDate = (date: ISODateString): NumericDate => // F.eks. "05.01.25"
const renderAlphanumericDate = (
date: ISODateString,
): AlphanumericDate => // F.eks. "5. jan. 2025"
// Ugyldig ❌
renderAlphanumericDate('2025-01-05')
// Type error: Argument of type 'string' is not assignable to
// parameter of type 'ISODateString'
const isoDate = '2025-01-05T12:00:00Z' as ISODateString
// Gyldig ✅
renderAlphanumericDate(isoDate) // "5. jan. 2025"
renderNumericDate(isoDate) // "05.01.25"
Når vi jobber med prosent, er det lett å blande sammen heltall og desimaltall – for eksempel kan både 50
og 0.5
bety "50 %", men i ulikt format. Med branded types kan vi tydelig skille mellom disse og unngå feilbruk.
type Percentage = Brand<number, 'Percentage'> // F.eks. 50
type DecimalFraction = Brand<number, 'DecimalFraction'> // F.eks. 0.5
const percentToFraction = (percentage: Percentage): DecimalFraction =>
(percentage / 100) as DecimalFraction
const fractionToPercent = (fraction: DecimalFraction): Percentage =>
(fraction * 100) as Percentage
// Ugyldig ❌
percentToFraction(0.25) // Type error: Argument of type 'number' is not assignable to parameter of type 'Percentage'
const discountPercentage = 25 as Percentage
const discountDecimal = 0.25 as DecimalFraction
// Gyldig ✅
percentToFraction(discountPercentage) // => 0.25
fractionToPercent(discountDecimal) // => 25
Semantisk betydning
En av de største fordelene med branded types er at de legger til et semantisk lag på toppen av underliggende typer. Dette gjør koden mer selvforklarende og lettere å forstå for både deg selv og andre på teamet. La oss se på et eksempel:
// Uten branded types ❌
function calculateDiscount(price: number, discount: number) {
return price * (1 - discount)
}
Selv om discount
er av typen number
kan det være uklart hva discount
representerer - er det et desimaltall, prosenttall eller et beløp?
// Med branded types ✅
type Price = Brand<number, 'Price'>
type DiscountPercentage = Brand<number, 'DiscountPercentage'>
function calculateDiscount(
price: Price,
discount: DiscountPercentage,
) {
return (price * (1 - discount / 100)) as Price
}
I dette eksempelet er det umiddelbart klart at discount
er et prosenttall, og funksjonen returnerer en ny pris.
Legg til metainformasjon på objekter
Branded types kan også brukes på objekter for å gi mer presis semantikk. Et praktisk eksempel er å skille mellom rå API-data og data som er validert og klar til bruk:
type Invoice = { id: string; amount: number }
type ValidatedInvoice = Brand<Invoice, 'Validated'>
function validateInvoice(invoice: Invoice): ValidatedInvoice {
if (invoice.amount <= 0) throw new Error('Invalid amount')
return invoice as ValidatedInvoice
}
Dette fungerer fint når du vil signalisere at et objekt har gått gjennom et spesifikt steg, som validering, parsing eller formatering. Men i mange tilfeller kan det være like greit å legge til et felt, for eksempel validated: true
, slik at du også har runtime-data å stole på. Eksempelet viser at det er fullt mulig å bruke branded types på objekter, men etter min erfaring er nok bruksområdene absolutt størst for primitive typer.
Branded types i Zod: validér og brand i en operasjon
Zod er et TypeScript-bibliotek for validering av data som har innebygd støtte for branded types. Mens vi tidligere har sett på branded types kun på type-nivå, lar Zod oss validere og brande data i samme operasjon:
import { z } from 'zod'
// Definerer Zod-skjemaer med validering og branding
const emailSchema = z.string().email().brand<'Email'>()
const ageSchema = z.number().min(0).max(120).brand<'Age'>()
// Infererer branded types fra skjemaene
type Email = z.infer<typeof emailSchema>
type Age = z.infer<typeof ageSchema>
// Validerer og brander data i samme operasjon
const email = emailSchema.parse('test@example.com') // Type: Email
const age = ageSchema.parse(25) // Type: Age
// Type error - ugyldig e-post ❌
const invalidEmail = emailSchema.parse('not-an-email') // Throws ZodError
// Type error - ugyldig alder ❌
const invalidAge = ageSchema.parse(150) // Throws ZodError
Dette gir oss noen fordeler:
- TypeScript vet at data er både validert og branded
- Vi får runtime-validering med TypeScript type-sikkerhet
- Feilmeldinger er tydelige og beskrivende
- Dersom data ikke består validering så fanger vi opp feil tidligere enn om vi gjorde validering kun i funksjonen hvor data senere ble brukt
Bedre AI-hjelp med tydeligere typer
Branded types fungerer spesielt godt sammen med AI-verktøy, fordi de gir eksplisitt informasjon om hva en verdi betyr, ikke bare hva slags type den er. Ved å tilføre ekstra mening til verdier, bygger de opp konteksten AI-verktøy bruker for å forstå koden – noe som gjør det lettere å fange opp intensjonen bak og unngå forslag som ikke gir mening.
Fordelene med å bruke branded types i TypeScript
- Forhindrer feil – hindrer at variabler av like underliggende typer blandes ved en glipp.
- Øker lesbarheten – gir variabler en tydelig, semantisk mening som gjør koden enklere å forstå.
- Sømløs validering – kan kombineres med f.eks. Zod for å validere og “brande” data i samme operasjon.
- Bedre AI-støtte – gir verktøy som Copilot og ChatGPT bedre kontekst, slik at de kan foreslå mer presise løsninger.
- Skalerbart – lett å starte i det små, og gradvis utvide bruken etter behov.
Hvis du ikke allerede bruker branded types vil jeg varmt anbefale deg å teste det ut i prosjektet du jobber på. Begynn i det små – én eller to typer – og utvid etterhvert. Som med flytteesker virker merkelapper kanskje overflødig når du bare har noen få, men når prosjektet vokser, blir du glad for at alt er organisert og forståelig. Små, smarte grep som dette gjør at koden får en mer solid grunnmur, og vi kan senke skuldrene litt og fokusere mer på å bygge gode brukeropplevelser. God «branding» 👋🏼
Del kunnskapen
Har du en kollega som også hadde dratt nytte av denne artikkelen?
Skrevet av
Mer fra Fag i Bekk
Nå er du ved veis ende. Gå til forsiden hvis du vil ha mer faglig påfyll.
Til forsiden