Hoppa till innehåll

TypeScript och WebAssembly

Två ord dyker upp så fort du läser om modern webbutveckling: TypeScript och WebAssembly. Det första lägger till typer ovanpå JavaScript. Det andra gör det möjligt att köra kod från andra språk i webbläsaren. De hänger ihop genom en och samma fråga: vad vet man om typerna, och när vet man det?

Tidsåtgång: Ungefär 12 minuter.

JavaScript är dynamiskt typat. Det är värden som har typer, inte variabler, så samma variabel kan innehålla först ett tal och sedan en sträng:

let greeting = 'Hello!'
greeting = 2 // Inget fel.

const ändrar inte på det. Det hindrar bara att variabeln tilldelas på nytt.

I ett statiskt typat språk som Java bestäms typen när koden skrivs, och kompilatorn vägrar att översätta kod som bryter mot den:

String greeting = "Hello!";
greeting = 2; // error: incompatible types: int cannot be converted to String

Avvägningen är inte självklar. Statiska typer fångar fel innan programmet körs och fungerar som dokumentation som alltid stämmer. Dynamiska typer är snabbare att komma igång med och ger färre rader att skriva. Om du läser en kurs i Java längre fram får du känna på skillnaden själv.

Du behöver inte byta språk för att få hjälp. JSDoc-kommentarerna du redan skriver innehåller typer, och VS Code kan kontrollera koden mot dem. Det enda som krävs är en kommentar överst i filen:

// @ts-check
/**
* Returns the number of tiles on a board.
*
* @param {number} rows - The number of rows.
* @returns {number} The number of tiles.
*/
function countTiles(rows) {
return rows * 4
}
countTiles('4')
// Argument of type 'string' is not assignable to parameter of type 'number'.

Felet syns direkt i editorn, och koden är fortfarande vanlig JavaScript. Det är samma motor som kontrollerar TypeScript som gör jobbet.

TypeScript, från Microsoft 2012, är JavaScript med typer inskrivna i själva koden:

function countTiles(rows: number): number {
return rows * 4
}

Det är inget nytt språk från grunden. Tar du bort typerna har du JavaScript. Dessutom tillkommer sådant som interface, ett sätt att beskriva hur ett objekt ska se ut.

Webbläsaren förstår inte TypeScript, så det måste översättas. Det går att göra med kompilatorn tsc, som sedan version 7 från 2026 är omskriven i Go och många gånger snabbare. Vanligare i ett webbprojekt är att byggverktyget gör det. Vite tar bort typerna från .ts-filer utan att kontrollera dem, så själva kontrollen sköter editorn och ett separat tsc --noEmit.

Utanför webbläsaren har det hänt mycket. Deno, Bun och sedan version 22.18 även Node.js kör TypeScript-filer direkt, genom att ta bort typerna. Det finns också ett förslag om att JavaScript självt ska tillåta typer som ignoreras när koden körs, men det befinner sig på ett tidigt stadium.

Koden är skriven i TypeScript och kompilerar utan fel. Servern har just bytt så att score skickas som strängen "12". Vad händer?

interface Player {
score: number
}
const response = await fetch('/api/player')
const player: Player = await response.json()
console.log(player.score + 1)

Det skrivs ut 121, utan fel. Typerna finns bara medan koden kontrolleras och är borta när den körs, och response.json() kan vara vad som helst. Med raden const player: Player lovar du kompilatorn hur datan ser ut, men ingen kontrollerar löftet.

Data som kommer utifrån behöver alltså kontrolleras när programmet körs, oavsett språk.

WebAssembly, ofta förkortat Wasm, är ett binärt format som webbläsaren kör bredvid JavaScript. Det släpptes i alla de stora webbläsarna 2017 efter ett samarbete mellan bland andra Google, Mozilla, Microsoft och Apple. Du skriver sällan formatet själv, utan kompilerar från ett annat språk som C, C++ eller Rust. Det finns också AssemblyScript, ett språk med TypeScripts syntax som är gjort för att kompileras till WebAssembly. Vanlig TypeScript går däremot inte att kompilera dit.

En modul kan se ut så här i textformatet:

(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))

Kompilerad till add.wasm och placerad i public/ laddas den från JavaScript, och funktionen anropas som vilken funktion som helst:

const { instance } = await WebAssembly.instantiateStreaming(fetch('./add.wasm'))
console.log(instance.exports.add(2, 3)) // 5

WebAssembly körs i samma sandlåda som JavaScript och har inte tillgång till något mer. Det kan inte ens röra DOM:en utan att gå via JavaScript.

Det används framför allt till två saker:

  • Att låna kod från andra språk. Ett bibliotek för bildbehandling skrivet i C går att kompilera och använda på webben, i stället för att skrivas om.
  • När prestandan verkligen spelar roll. Google Earth körs i webbläsaren med hjälp av WebAssembly, och många spelmotorer kan kompileras till det.

Det ersätter inte JavaScript. Varje anrop mellan JavaScript och WebAssembly har en kostnad, och för vanligt arbete med DOM:en vinner du ingenting.

add ovan tar två heltal, i32. Vad blir instance.exports.add('2', 3.9)?

Det blir 5, utan fel. WebAssembly-modulen är strikt typad, men gränsen mot JavaScript är det inte. Värdena konverteras när funktionen anropas: strängen '2' blir talet 2 och 3.9 kapas till 3.

Typerna skyddar alltså koden inuti modulen, men inte det du skickar in från JavaScript.

Varför JavaScript är svårt att göra snabbt

Section titled “Varför JavaScript är svårt att göra snabbt”

Att JavaScript är snabbt i dag beror på att motorn gissar. I V8, motorn i Chrome och Node.js, passerar koden flera steg:

Diagram över två flöden i V8. JavaScript går från Ignition, som tolkar bytekod, till Sparkplug, som ger snabb maskinkod, vidare till Maglev, som optimerar, och till TurboFan, som optimerar mest. En streckad pil går från TurboFan tillbaka till Ignition och kallas deoptimering, när ett antagande inte höll. WebAssembly går från Liftoff, som ger snabb maskinkod, till TurboFan.

Ignition tolkar koden och noterar vilka typer som faktiskt förekommer. Kod som körs ofta skickas vidare till kompilatorerna, och TurboFan bygger snabb maskinkod utifrån antagandet att typerna fortsätter vara desamma. Kommer det plötsligt en sträng där det alltid har varit ett tal går antagandet inte att lita på, och koden måste backa. Det kallas deoptimering.

WebAssembly har typerna givna från början. Liftoff översätter snabbt till maskinkod och koden kan börja köras direkt, och TurboFan optimerar sedan det som körs ofta. Eftersom typerna redan är kända behöver vanlig kod i WebAssembly inte gissa på samma sätt. Helt utan gissningar är det inte längre, eftersom V8 sedan Chrome 137 även använder deoptimering för vissa anrop i WebAssembly, men skillnaden i förutsägbarhet består.

I praktiken betyder det här sällan något för koden du skriver. JavaScript räcker för nästan allt i en webbapplikation, och när något är långsamt beror det oftast på något annat än språket.

  • I ett dynamiskt typat språk hör typen till värdet, i ett statiskt typat till variabeln.
  • Med // @ts-check och JSDoc får du typkontroll i vanlig JavaScript.
  • TypeScript är JavaScript med typer som tas bort före körning. Det kontrollerar inte data som kommer utifrån.
  • WebAssembly kör kompilerad kod i webbläsaren, bredvid JavaScript och inte i stället för det.
CCBY