Dette innlegget ble opprinnelig publisert på Variants blogg.
Et stadig tilbakevendende problem ved bruk av TypeScript, som kanskje ikke alle tenker over hele tiden, er at TypeScript er jo egentlig bare sukker på toppen av JavaScript. Og når sukkeret blir tømt ut av fatet, så står du kun igjen med grøten.
Eller sagt på en annen måte, når TypeScript-koden din transpileres til JavaScript, så fjernes alle TypeScript-typene fra koden. Og det du står igjen med er dynamisk kode ved kjøretid.
Selv om TypeScript kan gi deg veldig nyttige feilmeldinger når du skriver kode, spesielt i forhold til type-sikring — så er du på ingen måte sikret at det du skriver overholdes ved kjøretid.
Det betyr ikke at TypeScript er verdiløst så klart, på ingen som helst måte. Bare at du må ha et forhold til at ved kjøretid så er koden like dynamisk som om du hadde skrevet ren JavaScript.
Skrive kode som også er sikker ved kjøretid
Men det finnes måter en kan få sikker kode ved kjøretid også. Og her er det et relativt enkelt mønster som en kan bruke i TypeScript, som er såpass nyttig, men (kanskje?) såpass underkommunisert at jeg følte det fortjente sin egen liten blog post!
For å eksemplifisere dette, så bruker jeg et konsept om “prisområder” fra strømbransjen. Som flere begynner å få et forhold til med høye strømpriser, så har Nord Pool definert fem prisområder for handel av strøm innenfor Norge.
Disse prisområdene har hver “identifikator” og hver dag så oppdateres timesprisene/spotprisene for hvert område. Hvis jeg skulle ha laget en enkel TypeScript-type for å si noe om hva som er gyldig identifikator for hvert område, så kunne et naivt forsøk på dette være å lage en string literal union:
type PriceArea = 'NO1' | 'NO2' | 'NO3' | 'NO4' | 'NO5'
Denne kan jeg bruke videre i en funksjon som type for parameteren inn til funksjonen. Da vil TypeScript klage om jeg prøver å sende inn noe annet:
function getSpotPrices(priceArea: PriceArea) {
//...
}
getSpotPrices('asdf'); // her vil TypeScript klage
Men problemet her er at hvis 'asdf' ikke hardkodes, men for eksempel kommer inn som en query-parameter i en HTTP request til API-et ditt, eller hentes fra en database, så er du på ingen måte sikret ved kjøretid at det er en gyldig inn-parameter.
Type-sikkerheten vi hadde når vi skrev koden er borte og overholdes ikke ved kjøretid, siden type PriceArea fjernes fra JavaScript-koden. Hadde det ikke da vært kult om vi hadde en måte å passe på at type-sikringen overholdes ved kjøretid, og samtidig får hjelp av type-systemet til TypeScript ved kode-tid?
En bedre løsning
Svaret er at vi må ha noe tilgjengelig ved kjøretid som lar JavaScript sikre at inn-parameter er korrekt. Hvis en skulle ha enkelt definert prisområdene da, så kan det være greit å bruke en array:
const priceAreas = ['NO1', 'NO2', 'NO3', 'NO4', 'NO5']
Da er det ganske enkelt å sjekke ved kjøretid om inn-parameter er et gyldig prisområde:
function getSpotPrices(priceArea: PriceArea) {
if (priceAreas.includes(priceArea)) {
// .. kjør på!
} else {
// oi her må vi håndtere feil
}
}
Men blir ikke dette dobbelt opp? Må vi definere både en array for kjøretid, og en type for kode-tid, og holde disse i sync? Nei — fordi du kan faktisk utlede typen fra de definerte verdiene i array!
// "as const" på slutten forteller TypeScript at arrayen ikke endres på
const priceAreas = ['NO1', 'NO2', 'NO3', 'NO4', 'NO5'] as const
// vi utleder en type fra arrayet sine verdier
type PriceArea = typeof priceAreas[number]
Og med dette er vi sikret at de alltid er i sync.
Samtidig er det en finurlighet til vi kan gjøre for å forenkle sjekking av verdier ved kjøretid. I stedet for å ha et huske på overalt at det er en “hellig” array en plass i koden som har de korrekte verdiene, som sier noe ved kjøretid om at en verdi overholder en viss type-definisjon, så kan dette heller overholdes av en “type guard” funksjon.
function isPriceArea(area: string): area is PriceArea {
return priceAreas.includes(area as PriceArea)
}
Her er nøkkelen å definere at inn-parameter er av en viss type. Altså area is PriceArea. Og så vil logikken i funksjonen passe på at det faktisk er sant ved kjøretid.
En annen fordel ved å lage en “type guard” funksjon er at TypeScript kan automatisk utlede hva typen er uten at du eksplisitt trenger å si det. Det betyr at du kan bruke isPriceArea i en if-else-statement, og TypeScript vil ved kode-tid skjønne hva typen er i if-blokka, og hva den er i else.
const area: string = 'NO1'
if (isPriceArea(area)) {
// ✅ her har TypeScript automatisk satt typen til area som PriceArea
// selv om den er en "string" over if-en
} else {
// ⛔️ her vil area fortsatt være en "string"
}
Et veldig anvendelig mønster
Med et slikt mønster så kan type-sikring og resten av applikasjons-logikken leve adskilt. Så oppsummert betyr det at en kan ha en modul/fil for typene:
const priceAreas = ['NO1', 'NO2', 'NO3', 'NO4', 'NO5'] as const
export type PriceArea = typeof priceAreas[number]
export function isPriceArea(area: string): area is PriceArea {
return priceAreas.includes(area as PriceArea)
}
Og så kan applikasjons-logikken leve en annen plass, men likevel dra nytte av korrekt type-sikring:
import { isPriceArea, PriceArea } from '@src/price-area/types'
export async function apiRequestHandler(request) {
const area = request.query.area as string
if (isPriceArea(area)) {
// ✅ type-guard har sikret at area faktisk er en PriceArea
return getSpotPrices(area)
} else {
// ⛔️ her vil vi ikke kunne bruke area som en "PriceArea" siden
// type guard funksjonen sier at den ikke kan være en PriceArea
throw new Error('Not a valid price area!')
}
}
function getSpotPrices(priceArea: PriceArea) {
// trenger ikke ha egen sjekk her lengre siden det håndteres
// på "ytterkanten" av løsningen
}
Mer avanserte brukstilfeller
Dette er en veldig enkel måte å type-sikre enkle verdier i TypeScript. For mer avanserte brukstilfeller, hvis en skal for eksempel validere og parse større objekter, så vil jeg anbefale å bruke et eget bibliotek til dette. Her synes jeg Zod er et veldig bra alternativ.