TypeScript has conquered web development, backend microservices, and command-line tooling. Yet, despite its expressive static type system, TypeScript has always remained an interpreted guest inside the JavaScript ecosystem.
Every time you deploy a TypeScript CLI or microservice, you are forced to package an entire JavaScript runtime: Node.js, Deno, or Bun. This introduces inevitable tradeoffs:
- Cold-Start Latency: Even the fastest JIT engines take dozens of milliseconds to initialize.
- Binary Bloat: Single-binary bundlers routinely produce 40MB to 90MB executables simply to bundle the V8 engine and node runtime.
- Memory Footprint: Running small helper utilities or serverless functions burns tens of megabytes of baseline memory before executing a single line of business logic.
To explore a future where TypeScript functions as a standalone, ahead-of-time (AOT) compiled language, Vercel Labs has open-sourced ScriptC (vercel-labs/scriptc).
ScriptC is an experimental compiler that takes TypeScript and JavaScript and compiles them down to typed intermediate representations, readable C, LLVM IR, native machine code, and WebAssembly.
Here is an architectural look at how ScriptC works, what it supports, and how to compile your first native binary.
How ScriptC Works: The Multi-Stage Compilation Pipeline
ScriptC does not invent a new programming language. It leverages the official TypeScript compiler for syntax parsing and type checking, and then lowers the AST through progressively lower-level compiler tiers:
┌─────────────────────────────────────────────────────────────┐
│ TypeScript Source (`.ts`) │
└──────────────────────────────┬──────────────────────────────┘
│ (TypeScript Typechecker)
▼
┌─────────────────────────────────────────────────────────────┐
│ Typed IR (`hello.ir.json`) │
└──────────────────────────────┬──────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
[ Readable C ] [ LLVM IR ] [ WebAssembly ]
(`hello.c`) (`hello.ll`) (WASI Preview 1)
│ │
└─────────┬────────┘
▼
[ Native Assembly / Obj ]
(`hello.s` / `hello.o`)
│
▼
[ Standalone Native Executable ]
(Zero Node.js, Zero V8 Engine)
By allowing developers to inspect intermediate compiler stages via the --emit flag, developers can inspect their TypeScript code as:
--emit=ir: Typed JSON intermediate representation.
--emit=c: Clean, readable C source code.
--emit=llvm: Textual LLVM IR.
--emit=asm: Native assembly.
--emit=obj: Relocatable object files.
Key Architectural Highlights
1. Truly Static Native Builds
In a fully static build, ScriptC links a minimal runtime pack providing memory management and core primitives. There is no Node.js runtime, no V8 engine, and no JIT compiler embedded in the binary. The resulting binary starts in microseconds and uses negligible memory.
2. Native Implementation of Node.js APIs
ScriptC doesn't just compile pure algorithms; it compiles standard Node.js APIs directly to native system calls. For example, an HTTP server written using node:http:
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ status: "ok", path: req.url }));
});
server.listen(8080, () => {
console.log("Native HTTP server listening on http://localhost:8080");
});
When compiled with scriptc build server.ts -o server, ScriptC links the HTTP event loop directly against native socket primitives rather than passing through V8.
3. Static Coverage Diagnostics
Because TypeScript is dynamically typed by nature, not all idioms can be compiled statically ahead of time. ScriptC includes a built-in diagnostic tool to analyze code compatibility:
scriptc coverage app.ts
This command reports what percentage of the AST compiles statically and provides specific diagnostic codes for any dynamic sites requiring reflection.
4. Dynamic Fallback via QuickJS-NG
For legacy npm packages or codebases relying heavily on dynamic any types, passing --dynamic embeds an ultra-lightweight JavaScript engine (QuickJS-NG) directly into the executable. This allows compatibility with third-party npm packages without requiring the full Node.js runtime.
5. Cross-Compilation to WebAssembly (WASI)
By integrating with Zig as a cross-target linker driver (SCRIPTC_CC=zigcc), ScriptC can cross-compile TypeScript directly into portable wasm32-wasi modules capable of running in any WASI runtime or edge worker.
Getting Started
Installation
ScriptC requires Node.js 24+ for its compiler driver and can be installed globally via npm:
npm install -g scriptc
Compiling Your First Binary
Create a standard TypeScript file (hello.ts):
const target = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`Hello, ${target}! Compiled statically via ScriptC.`);
Compile it into a standalone executable:
# Compile to a native binary
scriptc build hello.ts -o hello
# Run the native binary
./hello TerminalChai
You can now copy ./hello to any compatible machine without Node.js installed, and it will execute natively.
Summary
While ScriptC is currently positioned as an experimental research project by Vercel Labs, it represents an important milestone for TypeScript. By decoupling the language from the JavaScript runtime and treating TypeScript as an AOT-compilable systems language, ScriptC opens the door to hyper-fast CLIs, tiny microservices, and edge functions.