Running TypeScript without a build step (and where the types actually go)
Running TypeScript without a build step is not the differentiator it used to be. Node strips the types and runs what is left, without checking them. Bun executes them types-blind. oam strips and executes instantly too — oxc takes the annotations out, V8 runs the result, no build step and no loader hook — and it takes .tsx and .jsx, which Node v22 cannot run at all. But on plain .ts, all three runtimes will start your file. That part is settled.
The question worth asking is where the types go. In Node and Bun the answer is nowhere: the annotations are deleted or ignored, and nothing ever reads them. In oam, execution and checking are concurrent paths over the same file. oxc strips and V8 runs, immediately; meanwhile a warm TypeScript 7 (tsgo) sidecar type-checks the full program and streams diagnostics as it finds them. The check never blocks execution, so you get an unchecked runtime's start and a checked codebase's diagnostics from the same command.
Concurrent means exactly that: a type error can arrive after your program has already started doing work, because diagnostics never gate the run. When you want a gate — CI, a pre-commit hook — that is oam check, the same tsgo check against a warm daemon, without running anything.
The three commands
oam run hello.ts # strip + execute instantly, types stream concurrently
oam test # *.test.ts in fresh isolates, ODIF output for agents
oam check # tsgo type-check with a warm daemon
oam run is the path described above. For JSX it behaves the way a TypeScript toolchain expects: the automatic runtime targets react/jsx-runtime by default, matching tsc; compilerOptions.jsxImportSource in the nearest tsconfig.json is honored, with extends chains merged per-option; and a per-file @jsxImportSource pragma wins over the tsconfig value, per tsc's precedence.
oam test runs *.test.ts with a fresh isolate per file, plus mocking and fake timers. And every diagnostic these commands emit — parse, type, runtime, test, install — is ODIF: structured JSON with stable codes and typed repair plans that agents consume directly, while humans see the same stream pretty-printed.
One CLI edge worth knowing: oam file.js --flag value works and matches node exactly — use that form. oam run file.js --flag value is an error, because run follows the cargo convention, where flags after a subcommand belong to the subcommand until --.
The divergence most likely to bite: a typeless .js file is ESM
Running TypeScript is where oam and Node agree most. Plain JavaScript with no declared module type is where they diverge first. With no "type" field in the nearest package.json, oam treats a project .js file as an ES module. Node treats it as CommonJS.
// app.js — package.json has no "type"
const p = require('path');
// Node: works.
// oam: ReferenceError: require is not defined
Two things bound the blast radius. Files inside node_modules keep Node's CommonJS default, so dependencies resolve the way their authors intended — the ESM-first default applies to your own project files. And Node 22 will reparse a typeless .js as ESM when it sees module syntax, with a MODULE_TYPELESS_PACKAGE_JSON warning — so the ESM direction works in both runtimes, and only the require() direction diverges.
Fixes, in order of preference:
- add
"type": "commonjs"to yourpackage.json, - rename the file to
.cjs, or - convert it to
import.
require() of an ES module throws
require('./thing.mjs')
// oam: ERR_REQUIRE_ESM
// Node 22: works
Node 22 added synchronous require() of an ESM graph; oam has not implemented it yet. Use await import(). Unlike the ESM-first default above, which is a decision, this one is a gap, not a policy — it is on the roadmap, and Node is simply ahead here until it closes.
When to pick something else
oam is beta: breaking changes before 1.0 are still possible and are called out in the changelog, and there is no LTS yet, so this is not the runtime for a service you are on-call for. If you need native addons or the full ecosystem surface, use Node. If single-process throughput is the axis you are optimising, or you want the bundler, test runner and package manager in one binary, benchmark Bun. If you want Deno's standard library or deno deploy, use Deno — those are not goals here.
docs/node-divergences.md, verified against Node v22.22.2. If oam surprises you and the surprise is not on that list, it is a bug — file it.