Introduction to Advanced WebAssembly and TypeScript Integration
WebAssembly (Wasm) has revolutionized web development by allowing code to run at near-native speed directly in the browser. However, at an advanced level, developers face a challenge: how to effectively combine TypeScript's strict typing with Wasm's low-level capabilities? In this article, we will explore techniques that go beyond basic examples: manual memory management, creating fully typed wrappers, optimizing calls, and building complex projects.
This material is intended for developers already familiar with the basics of Wasm and TypeScript. We will not dwell on compiling "Hello World" — we will focus on real-world tasks: working with buffers, data structures, multithreading, and profiling.
1. Advanced Memory Management: Manual Allocation and Structures
A basic example of exporting a function from Wasm to TypeScript usually works with primitives. At an advanced level, you will need to pass complex data: strings, arrays of structures, images. Let's look at how this is done using WebAssembly.Memory.
1.1. Memory Allocation and Deallocation
Suppose we have a Wasm module written in AssemblyScript or C that exports malloc and free functions. In TypeScript, we create a wrapper:
// types.tsexport interface WasmMemory { malloc(size: number): number; free(ptr: number): void; memory: WebAssembly.Memory;}
// memory-manager.tsexport class MemoryManager { private wasm: WasmMemory;
constructor(wasm: WasmMemory) { this.wasm = wasm; }
allocateString(str: string): number { const encoder = new TextEncoder(); const bytes = encoder.encode(str); const ptr = this.wasm.malloc(bytes.length + 1); // +1 for null terminator const view = new Uint8Array(this.wasm.memory.buffer); view.set(bytes, ptr); view[ptr + bytes.length] = 0; // null terminator return ptr; }
readString(ptr: number): string { const view = new Uint8Array(this.wasm.memory.buffer); let end = ptr; while (view[end] !== 0) end++; const bytes = view.slice(ptr, end); return new TextDecoder().decode(bytes); }
freeString(ptr: number): void { this.wasm.free(ptr); }}This approach ensures that memory does not leak, and TypeScript fully controls the data lifecycle.
1.2. Working with Data Structures
To pass objects (e.g., a point with coordinates {x: f32, y: f32}), use DataView:
interface Point { x: number; y: number; }
export class PointManager { private wasm: WasmMemory; private readonly SIZE = 8; // 2 float32
constructor(wasm: WasmMemory) { this.wasm = wasm; }
createPoint(p: Point): number { const ptr = this.wasm.malloc(this.SIZE); const view = new DataView(this.wasm.memory.buffer); view.setFloat32(ptr, p.x, true); view.setFloat32(ptr + 4, p.y, true); return ptr; }
readPoint(ptr: number): Point { const view = new DataView(this.wasm.memory.buffer); return { x: view.getFloat32(ptr, true), y: view.getFloat32(ptr + 4, true) }; }}Important: always check memory alignment. Wasm requires 4-byte alignment for f32.
2. Typing Wasm Imports and Exports in TypeScript
Working with Wasm without typing is dangerous: you could pass a number instead of a pointer and get a segfault. Use strict interfaces.
2.1. Generating Types from Wasm
The wasm-objdump or wasm2wat tool allows you to obtain function signatures. For an AssemblyScript project, use a type generator:
// wasm-types.d.tsdeclare namespace WasmModule { interface Exports { add(a: number, b: number): numb