scriptc

Turn a TypeScript file into a standalone binary with no Node and no JavaScript engine

TypeScript-to-Native Compiler

Category
Developer tools
Audience
Developers
Language
TypeScript
Licence
Apache-2.0

Updated

scriptc is a TypeScript-to-native compiler from Vercel Labs that turns TypeScript and JavaScript source into typed IR, readable C, textual LLVM IR, native assembly and object files, standalone executables, or WebAssembly modules. It is aimed at developers who already write their command-line tools and small programs in TypeScript and would rather hand someone a single binary than a Node installation plus a folder of dependencies. The project calls itself an experiment and is published under Apache-2.0, so treat it as something to try on a side path rather than a settled part of a build pipeline.

What it does

The usual way to ship a TypeScript program is to ship the runtime with it: Node, a JavaScript engine, and whatever the program imports. scriptc removes that step for code that can be compiled ahead of time. A static build includes a small native runtime and nothing else — no Node, no JavaScript engine embedded in the output.

The compiler can stop at several levels, and each one is a useful artifact on its own:

  • a typed intermediate representation, for looking at what the compiler understood
  • readable C, if you want to inspect or reuse the lowered program
  • textual LLVM IR
  • native assembly and object files
  • a finished native executable
  • a WebAssembly module, via WASI Preview 1

Not every TypeScript program can be compiled statically. When something cannot be, scriptc reports it as a diagnostic instead of silently falling back. For npm packages and any-typed code there is an explicit escape hatch: the --dynamic flag embeds quickjs-ng, so the parts that need a real JavaScript engine still run — but you have opted into carrying one. The video demonstration also shows a coverage view, which tells you how much of a given program compiles fully static.

Supported targets are macOS, Linux, Windows and WebAssembly.

How it works

The front end is not a reimplementation. scriptc uses the TypeScript compiler itself for parsing and type checking, which means the type annotations you already write become the input to code generation rather than a thing that disappears at build time. The better typed the source, the more of it can be lowered to static native code — that relationship is the whole design.

From there the program is lowered through the typed IR into C or LLVM IR, then to assembly and objects, then linked. The toolchain story is deliberately self-contained. Source-level outputs require only Node. On macOS 15 or newer on arm64, ordinary LLVM-tier executables use scriptc's own bundled helper and a precompiled runtime pack; clang is present only as the platform linker driver and does not compile the program's C or the runtime's C.

Getting started

The compiler needs Node.js 24 or newer, and it is published on npm as scriptc.

What you need beyond that depends on how far down you want to go. --emit=ir, --emit=c and --emit=llvm need nothing but Node, so you can look at the compiler's output on any machine that can run the package. --emit=asm and --emit=obj use the matching optional platform helper that is installed alongside scriptc on supported macOS, Linux and Windows hosts, and for WASI — notably without requiring a system compiler, archiver, linker or SDK of your own.

The smallest useful experiment is the one from the video: take a single hello.ts, build it into an executable, and run the result. From there, --dynamic is the flag to reach for when a dependency drags in code the static path rejects.

When to use it / when not

It fits well when the program is small, self-contained and well typed: a CLI you want to distribute as one file, a WASI module, or a utility whose users should not need a Node install. It is also genuinely useful as a teaching tool — emitting C or LLVM IR for a piece of TypeScript and reading the result tells you a lot about what your types actually mean.

It fits badly when the program leans on the dynamic half of the language. Heavy npm dependency trees, reflection-style patterns and any everywhere all push you toward --dynamic, and at that point you are shipping an embedded engine again, which undercuts the reason to be here. And because the project is explicitly experimental, anything with a real deployment schedule should not depend on it yet.

If you write TypeScript tooling and have ever been annoyed that distributing it means distributing a runtime, scriptc is worth an afternoon. The honest framing is that it is a well-scoped experiment with a clear rule — types in, static native code out, diagnostics when that is impossible — rather than a drop-in replacement for your current build. Read the diagnostics it gives you on a real file; they are informative even if you never ship the binary.

More in Developer tools

All of Developer tools →