TypeScript setup
esrun does not execute TypeScript, nor does it inject ambient globals like Bun or process. esdev runs TypeScript directly — types erased, never checked — and the definitions below are what your editor and tsc --noEmit use.
One-command setup
esdev --install-types
It adds @opentf/esrun-types as a dev dependency and adds it to compilerOptions.types — creating a tsconfig.json if you don't have one, or merging into an existing one without touching your other settings.
Which package manager it uses is decided in the order those answers are worth anything:
"packageManager" in package.json | The project saying which one it uses — the field corepack reads. It is there before a lockfile is: a fresh clone, a scaffold, a CI job at its first step |
| The lockfile | bun.lock, bun.lockb, pnpm-lock.yaml, yarn.lock, package-lock.json — evidence of what actually installed |
| What is installed here | With the project silent, the question is which manager can run at all |
npm is the last word rather than the first: "npm is always there" is exactly the assumption that produces npm: command not found in a container that ships only bun. A declared manager that is not installed is still the answer — it prints the line to run rather than reaching for a different one, because installing with the wrong manager leaves the wrong lockfile behind.
What it configures
{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["@opentf/esrun-types"] }, "include": ["**/*.ts"] }
types is what editors and language servers load globally, so the runtime:* modules resolve everywhere, not just per-file.
With no lib, TypeScript uses the default for your target — lib.esnext.full for ESNext — which already includes the web APIs this runtime provides: fetch, URL, crypto, the streams, TextEncoder, and Temporal from TypeScript 7. Setting lib explicitly replaces that default, so a config naming only ["ESNext"] loses all of them.
npm install --save-dev @opentf/esrun-types
Then add "types": ["@opentf/esrun-types"] to compilerOptions, or put /// <reference types="@opentf/esrun-types" /> in one file.
Use it
Your editor now provides intellisense, inline docs, and type checking for every built-in module:
import { file } from "runtime:fs"; // Your IDE knows `text()` returns a Promise<string> const data = await file("./config.json").text();
Typechecking
esdev never checks types while it runs; your editor does, and so does:
esdev typecheck # tsc --noEmit, with the project's own TypeScript esdev typecheck --pretty false # arguments go to tsc untouched esdev test --typecheck # the tests, and the check, in one run
esdev typecheck runs the tsc the project installed, through its package manager (found as --install-types finds it), so the version that checks is the version the project uses. Its output is tsc's own.
Path aliases
compilerOptions.paths and baseUrl resolve the same way in esdev build, esdev test and esdev <file>. A JavaScript project states them in jsconfig.json, which is read when there is no tsconfig.json beside it. See Imports for the rules.
Class syntax ahead of the engine
accessor x = 1 | Compiled — into the private field and the getter/setter pair it is defined to be, on the prototype for an instance accessor and on the constructor for a static one. V8 has not shipped the keyword, so without this it would be a syntax error at load. |
@decorator | Refused, by name, with what to write instead. Standard decorators are a stage-3 proposal the engine has not shipped and the compiler does not yet lower; TypeScript's older experimentalDecorators form is a different language feature and compiling one as the other would change what runs. |
A web component that would use @customElement and @property says the same thing in plain JavaScript today — customElements.define(name, Class) and a static properties or observedAttributes — and both are read at definition time, the way a browser reads them.