typestrip docs api.md
5.3 kB · 109 lines
Markdown
at main

API #

import { transform, transformSync, UnsupportedSyntaxError } from "@nibling/typestrip";

transform(src, options?) #

Transforms TypeScript asynchronously.

  • With a string, it returns a Promise<string> of the whole output.
  • With an iterable or async iterable of chunks, it returns an AsyncGenerator<string>. The yielded pieces concatenate to the same output a whole-string call gives.
await transform("let x: number = 1;"); // "let x         = 1;"

let out = "";
for await (const piece of transform(["let x: numb", "er = 1;"])) out += piece;

transformSync(src, options?) #

The synchronous form. It takes a string or a sync iterable of chunks, and returns a string or a Generator<string>. An async iterable throws a TypeError that points at transform.

transformSync("let x: number = 1;");

for (const piece of transformSync(["let x: numb", "er = 1;"])) out += piece;

Chunks #

A chunk is a string or a Uint8Array of UTF-8. A byte stream may split a multi-byte character across chunks. typestrip holds the partial sequence until the next chunk completes it.

Node Readable streams and web ReadableStreams are async iterables of chunks, so you can pass them to transform as they are.

import fs from "node:fs";
for await (const piece of transform(fs.createReadStream("input.ts"))) process.stdout.write(piece);

const res = await fetch("/app.ts");
for await (const piece of transform(res.body)) out += piece;

Memory stays flat at any input size. The engine keeps only the state it needs to resume at the next chunk.

Options #

Option Type Default Meaning
mode "erase" | "downlevel" "erase" Which mode to run. Any other value throws a TypeError before any engine loads.
onError ({ start, end }) => void throw Called for each construct the mode can't handle. start and end are UTF-16 offsets into the whole input. The construct stays in the output.

Errors #

Without onError, the first unsupported construct throws an UnsupportedSyntaxError:

  • a whole-string transform call rejects its Promise;
  • a whole-string transformSync call throws;
  • a generator throws from next().
try {
  transformSync("enum E { A }");
} catch (e) {
  e instanceof UnsupportedSyntaxError; // true
  e.code; // "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX", the same code Node uses
  e.start; // 0
}

With onError, nothing throws for unsupported syntax. For chunked input, onError fires as each chunk's output comes through the generator.

await transform(src, { onError: ({ start, end }) => report(start, end) });

Input the engine can't parse at all throws a SyntaxError ("typestrip: could not parse input"). UnsupportedSyntaxError is a subclass of SyntaxError, so check for it first.

Loading #

Neither engine loads at import. Each mode's engine compiles the first time a call needs it, and only once. transform and transformSync share the compiled module, whichever loads it first.

Node and other hosts #

package.json picks the entry by export condition, not by runtime detection.

Condition Entry Loads the engine with
node src/index.js fs, so transformSync works on the first call
default (browsers, workers, Deno, workerd) src/browser.js fetch and WebAssembly.compileStreaming

There is no synchronous fetch. In the browser entry, transformSync throws until transform has loaded that mode's engine once. Await one transform call at startup if you need the sync API later.

Serve build/*.wasm as application/wasm. A bundler that resolves new URL("../build/erase.wasm", import.meta.url) picks the binaries up; Vite, esbuild with a file loader, and webpack 5 all do. To serve or precache them yourself, import them as @nibling/typestrip/erase.wasm and @nibling/typestrip/downlevel.wasm (for example, import.meta.resolve("@nibling/typestrip/erase.wasm")). examples/service-worker strips .ts modules in a service worker.

Both engines need WasmGC and fixed-width SIMD. Every engine that ships WasmGC also ships SIMD.

Concurrency #

Streams can run at the same time, interleaved or not. An engine instance keeps its parse state in wasm globals, so it runs one stream at a time. Each call or stream takes an instance from a per-mode pool and returns it when done. A new instance comes from the compiled module, which costs about 0.04 ms (0.2 ms for the first) and 256 KiB of memory on Node 24. The pool keeps every instance it gets back, so repeated bursts of concurrent streams reuse instances instead of creating new ones. One to two seconds after the last stream opens, a timer trims each mode's pool back to 4 idle instances. The timer never keeps a Node, Bun, or Deno process alive.

engine(mode?) #

Returns the raw wasm exports of mode's first pooled instance, creating it if needed. Tests and profiling scripts use it. It isn't part of the supported API.

Environment overrides (Node entry) #

TYPESTRIP_ERASE_WASM and TYPESTRIP_DOWNLEVEL_WASM point one mode at another wasm file. Each override affects only its own mode. The development tools use them to compare builds.