Skip to main content
Technology & EngineeringWebassembly268 lines

Assemblyscript

Writing WebAssembly modules using AssemblyScript, a TypeScript-like language that compiles to Wasm

Quick Summary34 lines
You are an expert in AssemblyScript for building WebAssembly applications.

## Key Points

- **`--runtime full`** — full GC runtime (default)
- **`--runtime stub`** — bump allocator, never frees (smallest binary, good for short-lived modules)
- **`--runtime minimal`** — reference counting without cycle collection
- **Use `--bindings esm`** — auto-generated ESM bindings handle memory management and type conversion, eliminating manual pointer work.
- **Prefer typed arrays over generic `Array`** — `Int32Array`, `Float64Array`, etc. map directly to Wasm memory without boxing overhead.
- **Use `unchecked()` in hot loops** — skipping bounds checks in performance-critical inner loops can yield significant speedups, but only when you can guarantee the index is in range.
- **Choose the right runtime** — use `stub` for short-lived computations or one-shot modules; use `full` when objects have complex lifecycles.
- **Annotate types explicitly** — AssemblyScript's type inference is more limited than TypeScript's. Explicit types prevent unexpected promotions or truncations.
- **Leverage `@inline` for small hot functions** — the `@inline` decorator forces inlining, reducing call overhead.
- **Ignoring integer overflow** — `i32` arithmetic wraps at 2^31. If you need larger values, use `i64` or `f64`.
- **Using `--runtime stub` with long-lived modules** — the stub runtime never frees memory. Long-running modules will eventually exhaust available memory.
- **Confusing `null` and `0`** — in AssemblyScript, `null` for reference types is a null pointer (0). Nullable types must be declared as `Type | null`.

## Quick Example

```bash
npm init
npm install --save-dev assemblyscript
npx asinit .
```

```
assembly/
  index.ts        # AssemblyScript source
  tsconfig.json   # AS-specific TS config
build/            # compiled output
asconfig.json     # compiler configuration
```
skilldb get webassembly-skills/AssemblyscriptFull skill: 268 lines
Paste into your CLAUDE.md or agent config

AssemblyScript — WebAssembly

You are an expert in AssemblyScript for building WebAssembly applications.

Overview

AssemblyScript is a TypeScript-like language designed specifically for WebAssembly compilation. It uses a strict subset of TypeScript syntax with Wasm-native types, making it accessible to web developers without requiring knowledge of C, C++, or Rust. AssemblyScript compiles directly to Wasm without an intermediate step, producing compact and fast binaries.

Core Concepts

Type System

AssemblyScript uses WebAssembly-native numeric types instead of JavaScript's number:

TypeDescriptionJS Equivalent
i8signed 8-bit integer
u8unsigned 8-bit integer
i16signed 16-bit integer
u16unsigned 16-bit integer
i32signed 32-bit integernumber (int)
u32unsigned 32-bit integernumber (uint)
i64signed 64-bit integerbigint
u64unsigned 64-bit integerbigint
f3232-bit floatnumber (float)
f6464-bit floatnumber
boolboolean (i32 under the hood)boolean
stringmanaged string

Memory Management

AssemblyScript provides a managed runtime with garbage collection. Objects, arrays, and strings are heap-allocated and reference-counted or GC-traced depending on the runtime variant:

  • --runtime full — full GC runtime (default)
  • --runtime stub — bump allocator, never frees (smallest binary, good for short-lived modules)
  • --runtime minimal — reference counting without cycle collection

Module Exports and Imports

Functions and classes are exported with the standard export keyword. The @external decorator handles imports from the host environment.

Implementation Patterns

Project Setup

npm init
npm install --save-dev assemblyscript
npx asinit .

This generates:

assembly/
  index.ts        # AssemblyScript source
  tsconfig.json   # AS-specific TS config
build/            # compiled output
asconfig.json     # compiler configuration

Basic Module

// assembly/index.ts

export function add(a: i32, b: i32): i32 {
  return a + b;
}

export function factorial(n: i32): i64 {
  let result: i64 = 1;
  for (let i: i32 = 2; i <= n; i++) {
    result *= <i64>i;
  }
  return result;
}

export function isPrime(n: u32): bool {
  if (n < 2) return false;
  if (n < 4) return true;
  if (n % 2 == 0 || n % 3 == 0) return false;
  let i: u32 = 5;
  while (i * i <= n) {
    if (n % i == 0 || n % (i + 2) == 0) return false;
    i += 6;
  }
  return true;
}

Working with Arrays

// assembly/index.ts

export function sumArray(arr: Int32Array): i32 {
  let sum: i32 = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += unchecked(arr[i]); // unchecked skips bounds check for performance
  }
  return sum;
}

export function createFloatArray(size: i32): Float64Array {
  const arr = new Float64Array(size);
  for (let i = 0; i < size; i++) {
    arr[i] = <f64>i * 0.5;
  }
  return arr;
}

Classes and Objects

// assembly/index.ts

export class Vec2 {
  x: f64;
  y: f64;

  constructor(x: f64, y: f64) {
    this.x = x;
    this.y = y;
  }

  magnitude(): f64 {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }

  normalize(): Vec2 {
    const mag = this.magnitude();
    if (mag == 0.0) return new Vec2(0, 0);
    return new Vec2(this.x / mag, this.y / mag);
  }

  add(other: Vec2): Vec2 {
    return new Vec2(this.x + other.x, this.y + other.y);
  }

  dot(other: Vec2): f64 {
    return this.x * other.x + this.y * other.y;
  }
}

Host Imports

// assembly/index.ts

// Import a function from the host
@external("env", "logMessage")
declare function logMessage(msg: string): void;

@external("env", "getTimestamp")
declare function getTimestamp(): f64;

export function run(): void {
  const start = getTimestamp();
  logMessage("Processing started");

  // ... do work ...

  const elapsed = getTimestamp() - start;
  logMessage("Done in " + elapsed.toString() + "ms");
}

Building

# Development build (debug info, assertions)
npx asc assembly/index.ts --outFile build/module.wasm --textFile build/module.wat --debug

# Production build (optimized)
npx asc assembly/index.ts --outFile build/module.wasm --optimize

# Smallest binary (aggressive optimization)
npx asc assembly/index.ts --outFile build/module.wasm --optimize --shrinkLevel 2 --runtime stub

Using from JavaScript

import loader from "@assemblyscript/loader";

const module = await loader.instantiate(fetch("build/module.wasm"), {
  env: {
    logMessage(msgPtr) {
      // loader provides __getString to read AS strings from memory
      console.log(module.exports.__getString(msgPtr));
    },
    getTimestamp() {
      return performance.now();
    },
  },
});

const { add, factorial, isPrime, Vec2 } = module.exports;

console.log(add(2, 3)); // 5
console.log(factorial(20)); // 2432902008176640000n (BigInt for i64)

// Working with classes through the loader
const vec = module.exports.__pin(module.exports.__new(16, Vec2_ID));
// Or use the higher-level bindings generated by asc --bindings esm

Using ESM Bindings (Recommended)

npx asc assembly/index.ts --outFile build/module.wasm --bindings esm
// Generated bindings handle string/object marshaling automatically
import { add, Vec2 } from "./build/module.js";

console.log(add(10, 20));
const v = new Vec2(3.0, 4.0);
console.log(v.magnitude()); // 5.0

Best Practices

  • Use --bindings esm — auto-generated ESM bindings handle memory management and type conversion, eliminating manual pointer work.
  • Prefer typed arrays over generic ArrayInt32Array, Float64Array, etc. map directly to Wasm memory without boxing overhead.
  • Use unchecked() in hot loops — skipping bounds checks in performance-critical inner loops can yield significant speedups, but only when you can guarantee the index is in range.
  • Choose the right runtime — use stub for short-lived computations or one-shot modules; use full when objects have complex lifecycles.
  • Annotate types explicitly — AssemblyScript's type inference is more limited than TypeScript's. Explicit types prevent unexpected promotions or truncations.
  • Leverage @inline for small hot functions — the @inline decorator forces inlining, reducing call overhead.

Common Pitfalls

  • Assuming TypeScript compatibility — AssemblyScript looks like TypeScript but is not. Union types, any, structural typing, and many TS features are not supported. It is a different language with similar syntax.
  • Ignoring integer overflowi32 arithmetic wraps at 2^31. If you need larger values, use i64 or f64.
  • Passing strings without the loader — AssemblyScript strings are managed objects in linear memory. Without the loader or ESM bindings, you must manually read/write UTF-16 encoded data from memory pointers.
  • Using --runtime stub with long-lived modules — the stub runtime never frees memory. Long-running modules will eventually exhaust available memory.
  • Confusing null and 0 — in AssemblyScript, null for reference types is a null pointer (0). Nullable types must be declared as Type | null.
  • Not pinning objects passed to the host — if you pass an object pointer to JavaScript, the GC may collect it before JS is done using it. Use __pin to prevent collection, and __unpin when done.

Core Philosophy

AssemblyScript's value proposition is accessibility: it gives TypeScript developers a path to WebAssembly without learning C, C++, or Rust. But this accessibility comes with a critical caveat — AssemblyScript looks like TypeScript but is a fundamentally different language. It has a strict, Wasm-native type system, different memory management semantics, and no support for many TypeScript features like union types, any, or structural typing. Treat it as its own language with familiar syntax, not as "TypeScript that compiles to Wasm."

Choose the right runtime variant for your workload. The full GC runtime handles complex object lifecycles but adds binary size. The stub runtime produces the smallest binaries and is perfect for short-lived computation modules, but it never frees memory. The minimal runtime offers a middle ground with reference counting. This is not a one-size-fits-all decision — evaluate it per module based on expected lifetime and allocation patterns.

The ESM bindings (--bindings esm) are the recommended path for JavaScript interop. They automatically handle string marshaling, object lifecycle, and type conversion, eliminating the manual pointer arithmetic that makes raw Wasm interop error-prone. Unless you need absolute control over the boundary, use the generated bindings and focus your energy on the computation logic instead.

Anti-Patterns

  • Assuming TypeScript compatibility — writing union types, any, optional chaining on non-nullable types, or structural typing and expecting it to compile; AssemblyScript supports a strict subset of TypeScript syntax with different semantics.

  • Using --runtime stub for long-lived modules — the stub runtime never frees memory, so modules that run for extended periods or process many allocations will eventually exhaust available memory.

  • Passing strings without the loader or ESM bindings — AssemblyScript strings are managed objects in linear memory; without the loader, you must manually read/write UTF-16 encoded data from memory pointers, which is tedious and error-prone.

  • Omitting explicit type annotations — AssemblyScript's type inference is more limited than TypeScript's; relying on inference can cause unexpected integer promotions, truncations, or compile errors.

  • Using unchecked() without guaranteed bounds safety — skipping bounds checks in array access improves performance but produces undefined behavior if the index is out of range; only use it in verified hot loops.

Install this skill directly: skilldb add webassembly-skills

Get CLI access →