Xo and TypeScript
On this page
Part of the comparison series that starts with Xo and Go. It is not a “which is better” page. TypeScript on Node.js is one of the most used stacks for web backends, with the largest package ecosystem there is and the same language in the browser and on the server. Xo is an experimental compiled language. This page compares writing a backend in each: the language, safety, deployment, and measurements, including the rows where Node does as well as Xo or better.
Versions. Claims about TypeScript are checked against the TypeScript handbook, and claims about Node.js against the Node.js documentation, as of 2026-10-23. The newest releases that day were TypeScript 7.0 and Node.js 26.12.1 (Current) and 24.22.1 (LTS). The measurements used Node.js 22.22.1 and TypeScript 5.9.3, the versions pinned in the benchmark’s package-lock.json; they will be rerun on a current LTS. The Xo in the measurements is a build of main between 0.26.0 and 0.27.0 (it reports itself as 0.26.0). Last verified: 2026-10-23.
Short version
Choose TypeScript when the team already writes it, when code is shared with a browser front end, when you need npm’s ecosystem, fast iteration without a compile step for running code, or the tooling and hosting that support Node everywhere. It is mature and production proven; Xo is experimental (spec draft 0.10, no 1.0 yet).
Consider Xo when you want types the program cannot run without
(TypeScript’s are erased and checked only by tsc), errors in function
signatures, integers that do not lose precision, all CPU cores in one
process, and one static binary to deploy. In the measurements below Xo
was 2 to 9 times faster than Node on compute, concurrency and JSON,
served 4 times the requests of a single Node process at the median,
and used a small fraction of its memory.
What each is for
- TypeScript: “JavaScript with syntax for types” (typescriptlang.org). Node.js: “an asynchronous event-driven JavaScript runtime”, “designed to build scalable network applications” (About Node.js).
- Xo: backends, tools, and web services, designed so AI agents can write, check, cross compile, and debug programs with fast, structured feedback (Why Xo?).
The language
| TypeScript | Xo | |
|---|---|---|
| Types at run time | Erased: “Type annotations never change the runtime behavior of your program” (handbook). Node runs .ts files by stripping the types, and “no type checking is performed” (Node TypeScript docs); checking is a separate tsc step | Checked by the compiler; a program that does not check does not build or run |
| Absent values | null and undefined; “By default, values like null and undefined are assignable to any other type”, unless strictNullChecks is on (handbook) | No null; T? is Option[T], and the compiler makes you handle None (core 2.5) |
| Errors | Exceptions; a function’s type does not say what it throws | T ! E; an unhandled Result does not compile; ? propagates and records an error trace (core 2.6, 11.1) |
| Sum types | Unions with narrowing, discriminated by a field; exhaustiveness by a never check | Enums with payloads and exhaustive match, with guards (core 2, 3) |
| Numbers | One number type, a double: integers are exact only up to 2^53 − 1 (MDN); bigint for more | Int (64 bit, overflow faults), sized integers, Float, and BigInt in std/big (core 2.1, std 33) |
| Values | Objects and arrays are shared references | Value semantics with copy-on-write; mutation is visible in signatures (var parameters) (core 5) |
| Concurrency | One JavaScript thread per process with an event loop and async/await; more cores through worker_threads, child_process or cluster (About Node.js) | Tasks spread over every core in one process; structured concurrency (scope), cancellation without a parameter, channels, select, Mutex (core 7) |
| Side effects | Not in signatures | Effects in signatures (uses (net, fs)), capability values passed from main, and fakes for tests (core 6) |
Memory
- Node runs JavaScript on V8, with a tracing garbage collector.
- Xo native builds use reference counting: a value is freed when its last reference goes, with no collector pauses. HTTP handlers get request scoped arenas by default (FAQ, core 10).
- Measured: a running Node program started at about 40 MB of RSS in these benchmarks, Xo’s at 2.6 to 7.4 MB; the CRUD server peaked at 63.5 MB against 18.4 MB (sources below).
Safety and security
| TypeScript on Node | Xo | |
|---|---|---|
| Memory safety | Memory safe (a garbage collected runtime) | Memory safe; C reached through use c is outside the guarantee and listed by xo audit |
| Data races | JavaScript on one thread has none; memory shared between workers (SharedArrayBuffer) needs Atomics | Values never alias mutably, so data races are impossible except through Mutex and Atomic, which are synchronized (core 5) |
| Integer overflow | Above 2^53 − 1 a number silently rounds (MDN) | Faults: a bug report, not a wrong number (core 2, 8) |
| Capabilities | Any package can use the file system, network, and processes. Node’s --permission flag restricts them for the whole process (stable since 22.13), but it is “a ‘seat belt’ approach” that “does not protect against malicious code” (permissions) | Code reaches the file system, network, environment, or processes only through capability values it was given, per function; a module can be held to a ceiling (uses none) (core 6, Dependencies) |
| Supply chain | package-lock.json pins versions with integrity hashes; npm install runs a package’s preinstall, install, and postinstall scripts (npm scripts); npm audit reports known vulnerabilities | xo.lock hashes each module version; no dependency code runs at fetch or build setup (decision 0156); an effect lock fails an update that adds an effect until accepted, and xo audit lists each dependency’s effects (Dependencies). There is no vulnerability database yet |
Toolchain and deployment
- Running code. Node runs TypeScript directly (type stripping, on
by default since 22.18 and 23.6), with no build step;
tscchecks. Xo compiles to a native binary first (xo rundoes both). - Deploying. A Node service ships its source,
node_modules, and a Node runtime, or a single executable application, which Node marks “Active development” (SEA docs). Xo ships one static binary, andxo build --targets=allcross compiles for linux, darwin and windows on arm64 and x86-64, plus WASI, from any host (core 11). - Browser. TypeScript runs in every browser. Xo builds WASI programs, not browser front ends.
Build, size and startup (the CRUD server of the benchmarks below and a hello program; Node 22.22.1, TypeScript 5.9.3, esbuild 0.28.2; one build each, Apple M4 Pro, 2026-10-24):
| TypeScript on Node | xo 0.29.0 | |
|---|---|---|
| Cold build | tsc over the project 0.69 s; esbuild bundle of the server 0.37 s | 2.3 s (CRUD server) |
| Rebuild, nothing changed or one function edited | tsc 0.52 s (it keeps no cache); esbuild 0.16 to 0.17 s | 0.03 s unchanged, 0.50 s after an edit |
| Peak build memory | tsc 201 MB; esbuild 76 MB | 992 MB cold, 547 MB after an edit |
| What ships | a 3 KB bundle plus the 108 MB Node runtime, or a 107 MB single executable application (4.9 s to build) | a 2.3 MB binary (xo build --release; 6.2 MB with debug information) |
| Hello startup, wall | 46.2 ms | 2.8 ms (0.061 of Node’s) |
| Hello startup, peak memory | 36.5 MB | 2.6 MB |
Source: twins-build-2026-10-24.md, lines 20, 26 to 27, 31, 39 to 40, 49 to 51 and 66 to 69.
A cold build and a build after an edit are faster in TypeScript and use far less memory (Xo’s no-change build, 0.03 s, is the exception); Xo’s program
starts 16 times faster and ships as one file of 2.3 MB (xo build --release) against
Node’s 107 MB single executable. The single executable application was
checked by starting it and creating a user.
Writing it with AI agents
TypeScript is among the languages models know best, and its editor tooling is excellent. Xo is designed for the agent’s loop:
xo check --jsonreports every problem with a stableXOnnnncode and machine-applicable fixes; codes never change meaning (diagnostics).- TypeScript habits (
any, arrow functions, C stylefor,await, camelCase locals, and others, 29 cases) get a diagnostic that names TypeScript and gives the Xo form, with a fix. xo def,xo refs,xo query,xo edit, andxo mcpanswer semantic questions and make edits through resolved symbols.
Benchmarks
Method. The same programs written twice, each in its own language’s
idiom (Node: async tasks on the event loop for spawn, fanout and
ping-pong; worker_threads with a SharedArrayBuffer and Atomics for
mutex and parallel map; JSON.parse and JSON.stringify; node:http
for the server), and run under xo bench --ab with Node as A and Xo as
B, 10 interleaved rounds; a verdict is “better” or “worse” only when the
95% interval excludes 1.0, and the file asks that ratios within 10% be
read as ties. Every program prints a checksum and the run stops if
Node’s and Xo’s differ. There is no rwmutex row: Node has no read write
lock. Machine: Apple M4 Pro (12 cores), macOS 27, darwin/arm64,
with other jobs running (each row waited for a load under 5).
Versions: Node.js 22.22.1, TypeScript 5.9.3; Xo from main between
0.26.0 and 0.27.0, native backend. Date: 2026-10-23. Ratios are Xo
over Node; under 1.0 means Xo took less
(node-vs-xo-2026-10-23.md, lines 1 to 10 and 94 to 99).
| workload | wall time, Xo / Node | verdict | source |
|---|---|---|---|
| parallel map (worker threads) | 0.115 | better | line 43 |
| mutex (worker threads) | 0.116 | better | line 38 |
| JSON encode | 0.25 | better | line 61 |
| ping-pong | 0.34 | better | line 33 |
| fib (recursive, 40) | 0.38 | better | line 15 |
| JSON decode | 0.41 | better | line 51 |
| fanout | 0.49 | better | line 28 |
| spawn | 0.99 | within noise | line 23 |
Two caveats. The short rows include Node’s start (46 ms for a hello program, above), a large part of a 60 to 90 ms run; the fib and parallel map rows are long enough to show the engine. And Xo spreads tasks over every core where Node’s event loop uses one: on spawn the wall time was the same but Xo used 4.6 times Node’s CPU (line 24), and on fanout about twice (line 29, too coarse for a verdict). Xo’s peak memory was 0.035 to 0.17 of Node’s on every row (lines 18 to 69).
HTTP: the users CRUD API (node:http in one process, against Xo’s
std/http with request arenas), 32 workers on loopback, 10 s measured
after a 2 s warmup, the median of 3 runs
(lines 85 to 91):
| Node | Xo | Xo / Node | |
|---|---|---|---|
| requests/s | 30,891 | 124,648 | 4.0 |
| p50 latency | 528 µs | 214 µs | 0.41 |
| p99 latency | 9,857 µs | 1,023 µs | 0.10 |
| p99.9 latency | 27,468 µs | 2,613 µs | 0.095 |
| server CPU per request | 19.39 µs | 17.56 µs | 0.91 |
| peak RSS | 63.5 MB | 18.4 MB | 0.29 |
Read this table with its runs (lines 75 to 83). Node’s three runs were far apart: 80,335, 30,891 and 20,432 requests/s. In its best run Node used 12.10 µs of CPU per request, less than Xo’s 17.56 in the same round; Xo’s lead in throughput comes from using more than one core. Node is measured as one process because a cluster of workers would give each worker its own in-memory store, which is not the same program (line 98); a Node deployment behind a load balancer with a shared database would scale across cores that way.
These are a young compiler’s numbers on one machine. The comparison is
rerun from bench/perf/node/run.sh and this page’s tables are refreshed
from its results file.