Skip to content

Emscripten TypeScript Example

Emscripten TypeScript Example is a minimal, production-ready template for compiling C code to WebAssembly and calling it from TypeScript in the browser — with full type safety. It solves a common pain point in WebAssembly projects: the gap between raw .wasm binaries and the typed, ergonomic APIs that frontend developers expect. The project ships a complete pipeline from C source to deployed web page, including auto-generated .d.ts files, esbuild bundling, and GitHub Pages CI/CD.

A live demo is available.

The pipeline has three stages: compile, bundle, and serve.

1. Compile C → WebAssembly + TypeScript Definitions

Section titled “1. Compile C → WebAssembly + TypeScript Definitions”

The C source in src/lib.c is intentionally simple — the point is the toolchain, not the algorithm:

#include <emscripten/emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}

EMSCRIPTEN_KEEPALIVE prevents the compiler from dead-code-eliminating the function. The build.sh script runs emcc with carefully chosen flags:

Terminal window
emcc src/lib.c \
-Oz \
-s WASM=1 \
-s ENVIRONMENT='web' \
-s EXPORT_ES6=1 \
-s EXPORTED_FUNCTIONS="['_add']" \
-s NO_FILESYSTEM=1 \
-s ALLOW_MEMORY_GROWTH=0 \
-o src/lib.mjs \
--emit-tsd lib.d.ts

Key choices here:

  • -Oz — Aggressively optimise for binary size, critical for web delivery
  • ENVIRONMENT='web' — Strip Node.js-only code paths from the glue JS
  • EXPORT_ES6=1 — Output an ES module so it plays nicely with modern bundlers
  • NO_FILESYSTEM=1 — Remove Emscripten’s virtual filesystem (saves ~50KB of glue code)
  • --emit-tsd — The star of the show: auto-generates TypeScript definitions for every exported function

The build.mjs script orchestrates a two-phase build: first running the Emscripten compilation, then using esbuild to bundle the TypeScript entry point into a minified ES module with inline source maps. Static assets (lib.wasm, index.html) are copied to dist/.

The TypeScript side in src/index.ts imports the generated module and calls the C function with full type checking:

import init from './lib'
const promise = init()
async function main() {
const { _add: add } = await promise
const result = add(1, 2)
document.getElementById('result')!.innerText = result.toString()
}
main()

Because --emit-tsd generates a .d.ts alongside the .mjs glue code, TypeScript knows that _add takes two numbers and returns a number. No manual type declarations needed.

  • Auto-generated TypeScript definitions — Uses Emscripten’s --emit-tsd flag to produce .d.ts files, eliminating hand-written type stubs
  • Minimal WASM binary-Oz, NO_FILESYSTEM, and ALLOW_MEMORY_GROWTH=0 keep the output as small as possible
  • ES module output — Both the Emscripten glue and the final bundle use ESM, enabling tree-shaking and modern <script type="module"> loading
  • On-the-fly dev serverserve.mjs is a custom HTTP server that transpiles .ts to .js on each request via esbuild, so you never serve stale bundles during development
  • GitHub Pages CI/CD — A GitHub Actions workflow (pages.yml) installs the Emscripten SDK, builds the project, and deploys to GitHub Pages on every push to main
  • Zero runtime dependencies — Only esbuild, typescript, and @types/node as dev dependencies; the final output is self-contained
LanguageC, TypeScript
CompilerEmscripten (emcc)
Bundleresbuild
PlatformBrowser (WebAssembly)
CI/CDGitHub Actions → GitHub Pages
Dev Dependenciesesbuild 0.24, TypeScript 5.6

The source code is available on the project’s GitHub repository.