Xo is experimental. The source is now public on GitHub, or try Xo in your browser.
Menu · Xo and TypeScript

Docs

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

TypeScriptXo
Types at run timeErased: “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 stepChecked by the compiler; a program that does not check does not build or run
Absent valuesnull 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)
ErrorsExceptions; a function’s type does not say what it throwsT ! E; an unhandled Result does not compile; ? propagates and records an error trace (core 2.6, 11.1)
Sum typesUnions with narrowing, discriminated by a field; exhaustiveness by a never checkEnums with payloads and exhaustive match, with guards (core 2, 3)
NumbersOne number type, a double: integers are exact only up to 2^53 − 1 (MDN); bigint for moreInt (64 bit, overflow faults), sized integers, Float, and BigInt in std/big (core 2.1, std 33)
ValuesObjects and arrays are shared referencesValue semantics with copy-on-write; mutation is visible in signatures (var parameters) (core 5)
ConcurrencyOne 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 effectsNot in signaturesEffects 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 NodeXo
Memory safetyMemory safe (a garbage collected runtime)Memory safe; C reached through use c is outside the guarantee and listed by xo audit
Data racesJavaScript on one thread has none; memory shared between workers (SharedArrayBuffer) needs AtomicsValues never alias mutably, so data races are impossible except through Mutex and Atomic, which are synchronized (core 5)
Integer overflowAbove 2^53 − 1 a number silently rounds (MDN)Faults: a bug report, not a wrong number (core 2, 8)
CapabilitiesAny 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 chainpackage-lock.json pins versions with integrity hashes; npm install runs a package’s preinstall, install, and postinstall scripts (npm scripts); npm audit reports known vulnerabilitiesxo.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; tsc checks. Xo compiles to a native binary first (xo run does 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, and xo build --targets=all cross 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 Nodexo 0.29.0
Cold buildtsc over the project 0.69 s; esbuild bundle of the server 0.37 s2.3 s (CRUD server)
Rebuild, nothing changed or one function editedtsc 0.52 s (it keeps no cache); esbuild 0.16 to 0.17 s0.03 s unchanged, 0.50 s after an edit
Peak build memorytsc 201 MB; esbuild 76 MB992 MB cold, 547 MB after an edit
What shipsa 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, wall46.2 ms2.8 ms (0.061 of Node’s)
Hello startup, peak memory36.5 MB2.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 --json reports every problem with a stable XOnnnn code and machine-applicable fixes; codes never change meaning (diagnostics).
  • TypeScript habits (any, arrow functions, C style for, 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, and xo mcp answer 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).

workloadwall time, Xo / Nodeverdictsource
parallel map (worker threads)0.115betterline 43
mutex (worker threads)0.116betterline 38
JSON encode0.25betterline 61
ping-pong0.34betterline 33
fib (recursive, 40)0.38betterline 15
JSON decode0.41betterline 51
fanout0.49betterline 28
spawn0.99within noiseline 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):

NodeXoXo / Node
requests/s30,891124,6484.0
p50 latency528 µs214 µs0.41
p99 latency9,857 µs1,023 µs0.10
p99.9 latency27,468 µs2,613 µs0.095
server CPU per request19.39 µs17.56 µs0.91
peak RSS63.5 MB18.4 MB0.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.

↑ index