Scriptc by Vercel: TypeScript-to-Native compiler, no JavaScript engine in binary
Summary
Vercel Labs releases Scriptc, a compiler that transforms ordinary TypeScript into small, fast native executables without requiring Node, V8, or any JavaScript engine in the binary, supporting most of TypeScript's static features and Node's API surface.
View Cached Full Text
Cached at: 07/27/26, 04:38 AM
vercel-labs/scriptc
Source: https://github.com/vercel-labs/scriptc
scriptc
Zero-runtime TypeScript. scriptc compiles ordinary TypeScript into small, fast native executables — no Node, no V8, no JavaScript engine in the binary.
$ cat fib.ts
function fib(n: number): number {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(30));
$ scriptc run fib.ts
832040
$ scriptc build fib.ts && ls -la fib
-rwxr-xr-x 178K fib # a self-contained native binary, ~2ms startup
No changes to your code. No annotations, no dialect — the same TypeScript you run on Node, type-checked by the real TypeScript compiler and compiled to native. What compiles behaves byte-for-byte like Node.
Install
$ npm install -g scriptc
Requires clang (preinstalled with Xcode Command Line Tools). macOS arm64 is the primary platform; Linux and Windows binaries build by cross-compilation, each verified by its own differential test lane.
The idea: staticness you can see
Most TypeScript is far more static than the ecosystem assumes. scriptc decides, construct by construct, what can compile to native code — and tells you:
$ scriptc coverage app.ts
statements analyzed 4481
compile statically 4451 (99%)
blockers:
×2 functions with optional parameters as values SC1090
×1 Promise.reject SC2020
Three tiers, always explicit:
- Compiled statically — native code, no engine. The default, and the only mode unless you opt out.
- Runs dynamically (
--dynamic) — an embedded JavaScript engine (quickjs-ng, ~620KB) executes what can’t be static: npm dependencies’ shipped JS,any-typed code. Every value crossing back into static code is validated at runtime — a lying type throws a catchableTypeErrorinstead of corrupting memory. - Rejected — everything else fails with a specific error code, a code frame, and usually a rewrite hint. Nothing is ever silently miscompiled.
What compiles
The static surface covers the language and the standard library real programs use:
- The language — classes with single inheritance and true dynamic dispatch (devirtualized when provably safe), closures with JS capture semantics, generics (monomorphized), discriminated unions as tagged values driven by TypeScript’s own narrowing,
async/awaiton stackful fibers with JS-exact scheduling, exceptions withfinally, destructuring, spread, optional/default/rest parameters, getters/setters, iterators over strings/arrays/Maps/Sets, template literals, regular expressions (the engine is the same ECMAScript-exact bytecode interpreter QuickJS uses, linked only into regex-using binaries). - The standard library — strings with UTF-16-exact semantics, arrays/Maps/Sets with JS-exact ordering and identity,
JSONwith runtime-validated casts,Math, typed arrays andBuffer,Errorhierarchies with typedcatch. - Node’s API surface —
fs(sync and promises),path(byte-exact port),process,child_processwith piped streams,os,crypto,url/URL,zlib, timers and signal handlers on a dependency-free event loop — and the server stack:net,http,https,tls(vendored mbedTLS),dgram,dns,fs.watch,readline. Real proxy servers compile. fetchand the WHATWG web subset (streams,Headers,AbortSignal) over the same native net/TLS stack — redirects, gzip,AbortSignal.timeout, Node-shaped error causes; no libcurl, no system HTTP dependency.- npm dependencies (with
--dynamic) — packages resolve with Node’s own algorithm, typecheck against their shipped.d.ts, and their JS is embedded into the binary at build time. Binaries never readnode_modulesat runtime.
Programs typecheck against TypeScript’s real es2025 lib (plus @types/node when your project has it), and your tsconfig.json governs checker strictness. Anything reached that has no lowering is a precise diagnostic, never a surprise.
Correctness
Two enforcement mechanisms run on every change:
- Differential testing — every corpus program (800+ tests) runs under Node and as a native binary; stdout, stderr, and exit codes must match byte-for-byte. Number formatting is JS-exact (shortest-roundtrip, fuzz-verified against Node on a million doubles). Servers are tested with live client drivers against both implementations.
- Memory-safety lane — the entire corpus re-runs under AddressSanitizer with a reference-count audit; leaks and use-after-free are build failures.
The deliberate divergences from Node (there are a few dozen, mostly around timing internals and error-object properties) are documented and numbered; nothing diverges silently.
Performance
Measured on Apple M-series against the same workloads in Node, Go, Rust, and Zig (all byte-identical output, verified):
| dimension | scriptc | context |
|---|---|---|
| startup | ~2.4ms | Node: ~47ms; on par with Zig, ahead of Go/Rust |
| binary size | 170–200KB static, ~3MB with --dynamic + embedded deps | Go: ~2MB; Node SEA: 60–100MB |
| memory (RSS) | 1–4MB typical | Node: 67–116MB |
| runtime | JS-faithful f64 semantics; competitive with the systems languages on most workloads | integer inference and ownership analysis are on the roadmap |
Escape hatches
comptime(() => ...)runs TypeScript at build time (in an isolated VM inside the compiler) and bakes the result into the binary as a literal.- Native FFI (
--ffi) binds signature-only TypeScript declarations to direct C ABI calls and links manifest-declared archives, objects, and system libraries. The boundary is explicit and length-delimited; see the Native FFI guide. --dynamicembeds the engine for npm deps andanycode.scriptc coverage --dynamicreports exactly which statements run where and what the remaining blockers are. Static stays the default: a binary never silently grows an engine.- Checked casts —
JSON.parse(...) as Configinserts a runtime validation that throws a catchable error naming the offending path (expected number at $.port, got string). TypeScript’sasis a promise; scriptc verifies it.
Architecture
flowchart LR
TS[TypeScript] -->|tsc: parse + typecheck| L[lowering]
L --> IR[typed IR]
IR --> C[C]
C -->|clang| BIN[native executable]
packages/compiler— frontend (tsc API → IR), the IR with validator/serializer, the LLVM and C backends. The IR is the only interface between the ends; LLVM is the default code generator (with a transparent fallback for programs outside its tier), and C is the reference backend forever (readable, source-line-annotated output via--backend c).packages/runtime— the C runtime: refcounted values with a cycle collector, stackful fibers and the event loop (kqueue), the server stack, JS-exact number formatting. Feature units are link-gated: binaries pay only for what they use.packages/cli—scriptc build | run | coverage.
Development
$ pnpm install && pnpm build
$ pnpm test # differential corpus + diagnostics snapshots
$ SCRIPTC_SAN=1 pnpm test # the same corpus under ASan + RC audit
$ pnpm scriptc build x.ts --emit-ir # keep .scriptc/x.c and x.ir.json
Every feature lands with differential tests; both lanes green is the merge bar.
Similar Articles
@rauchg: I compiled 𝚟𝚎𝚛𝚌𝚎𝚕 CLI TypeScript to native with 𝚜𝚌𝚛𝚒𝚙𝚝𝚌. Incredible. ✓ Resulting binary size: 1.28mb ✓ Sta…
Guillermo Rauch compiled Vercel CLI TypeScript to native using scriptc, achieving a 1.28MB binary, 1.5ms startup overhead, and 2.94s compile time. scriptc is a TypeScript-to-native compiler written in TypeScript with full Node.js compatibility.
LemmaScript: A Verification Toolchain for TypeScript via Dafny
LemmaScript is a new toolchain that compiles TypeScript to Dafny for formal verification without altering the runtime, demonstrated by proving a CVE fix in the Hono framework.
Perry Compiles TypeScript directly to executables using SWC and LLVM
Perry compiles TypeScript directly to native executables using SWC and LLVM, producing small binaries with no runtime dependencies, supporting all major platforms and native UI widgets.
TypeScript 7
TypeScript 7 is a major release that rewrites the compiler in Go, achieving 8-12x faster build times while maintaining full compatibility, available now on npm.
microsoft/TypeScript
TypeScript is a language for application-scale JavaScript that adds optional types. The repository hosts the TypeScript compiler and tooling.