- Book: TypeScript in Production — Tooling, Build, and Library Authoring Across Runtimes
- Also by me: The TypeScript Library — the 5-book collection
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
You've seen it. A project switches its dev loop from `tsc-watch
- node --loader
to a singlebun run src/index.ts. The cold start drops from three seconds to two hundred milliseconds. Hot edits feel weightless. The team ships the change, deletes the watcher, and moves on. A week later a PR lands that introduces a real type error. APromisegets returned where the caller expectsUser. CI is green. The runtime is happy. The bug ships. Production gets a.then is not a function(or worse, a silentundefined`) ten minutes after deploy.
Teams misread what Bun actually does with their TypeScript.
Bun runs .ts and .tsx files without a build step. That is
true. From there, a tired team draws the wrong conclusion:
that Bun has replaced their type checker. It has not. Bun
transpiles TypeScript at runtime. It doesn't check types.
That distinction matters.
The rule is simple. The line between what Bun handles
natively and what still needs tsc is sharp and documented,
stable enough in 2026 to bet a CI matrix on.
What Bun does natively
Bun ships TypeScript as a first-class runtime input. There is
no tsconfig.json required, no ts-node shim, no --loader
flag. You point bun at a .ts file and it runs.
bun run src/server.ts
bun test
bun build src/index.ts --outfile dist/index.js
Under the hood, Bun's transpiler
parses the file, strips type annotations, lowers any modern
syntax the runtime needs lowered, and hands JavaScript to its
JavaScriptCore-based engine. The transpile is in Zig, the
output is cached, and the resulting startup time is what
people notice first.
Bun handles more TypeScript than people expect:
-
Type annotation stripping — every
: Foo, everyascast, everysatisfies. Pure erasure, no type semantics. -
Decorators — both the stage-3 standard
decorators and
the legacy
experimentalDecoratorsshape, picked up fromtsconfig.jsonwhen present. -
JSX / TSX — out of the box. Bun reads
jsx,jsxImportSource, andjsxFactoryfrom your tsconfig. -
pathsmapping —@/lib/*style aliases work without a separatetsconfig-pathsruntime hook. Bun's resolver reads the config and rewrites imports. -
usingandawait using— the explicit-resource-management syntax from TypeScript 5.2+ runs natively. Bun lowers it when needed for older targets. -
ESM, top-level await, JSON imports,
import.meta— the modern module surface works without flags. -
Source maps — emitted automatically so stack traces point
at your
.tslines, not the transpiled output.
The practical effect is that the inner dev loop no longer has
a build step. Edit, save, re-run. For library code, scripts,
servers, CLI tools — anything where the loop was previously
"tsc-watch in one terminal, node in another" — Bun collapses
both into one process.
That is where the speed comes from. The wall-clock saving on
a small project is dramatic. The saving on a monorepo with
project references is even bigger, because Bun does not care
about the project graph; it transpiles the file in front of
it.
What Bun deliberately skips
Here's the catch. Bun's documentation is upfront about it,
but the line in the docs is one sentence inside a longer
page, and people skim past. Bun's docs say plainly that it
does not type-check your code.
Concretely, Bun's transpiler treats TypeScript the same way
esbuild
does — as JavaScript with extra syntax that has to come out
before the engine sees it. No type graph gets built. Generics
aren't resolved. Nothing checks that the value returned from
getUser() matches the Promise<User> you typed. The
annotation gets stripped and the code runs.
The shortlist of what Bun will not do for you:
-
No type checking. A
stringassigned to anumberruns fine until the runtime trips over it. -
No
.d.tsemission. Bun has no declaration emitter. If you publish a library, you still needtsc --emitDeclarationOnly(or oxc / swc / a parallel declaration tool) to produce the declaration files your consumers' editors read. -
No
const enuminlining. Bun erases the values. If your code depends on the inlined-literal behavior, you get surprises. The fix is to not useconst enumin published libraries — which is the right fix anyway in 2026. -
No project references.
tsc -bwalksreferencesarrays and orchestrates incremental builds across packages. Bun treats every file as standalone. For a monorepo's type-correctness story, you still wanttsc -b --noEmit. -
No
tsc-shaped diagnostics. Bun reports parse errors and runtime errors. It does not report "Type 'X' is not assignable to type 'Y'."
The ESM/CJS interop surface is the other place to be careful.
Bun is more forgiving than Node about importing CommonJS from
ESM — require() works in .ts files even when tsconfig says
"module": "ESNext", and CJS default imports unwrap in ways
Node refuses to. Fine for an app that only runs on Bun. A
hazard for a library, because the same code can break when a
consumer pulls it into Node. Library authors should target
the strict ESM/CJS rules of their publish target. What Bun
lets you get away with isn't the contract your consumers run.
Library authoring: where tsc still owns the build
For a library that ships to Node, Bun, Deno, and bundlers, the
shape of the build pipeline in 2026 is two-stage:
-
Bundle / transpile — Bun, esbuild, tsup, or rolldown.
Whichever you pick, this stage produces the
.jsyour consumers run. -
Declaration emit —
tsc --emitDeclarationOnly(or oxc / swc with--isolatedDeclarationsif you've turned that flag on). This stage produces the.d.tsyour consumers' editors read.
The mistake is to think Bun replaces stage 2. It does not.
Bun has no declaration emitter, and there is no plan to add
one. Declaration emit needs the type checker, and the type
checker is the expensive part Bun is deliberately not
shipping. If you publish a package.json that points
"types" at a hand-rolled .d.ts, your consumers see whatever
you typed. If you publish without declarations, your consumers
get implicit any everywhere — and they will notice.
A working package.json for a library that uses Bun for
tests and bundling, and tsc for declarations:
{
"name": "my-lib",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build:js": "bun build src/index.ts --outdir dist --target node",
"build:dts": "tsc -p tsconfig.build.json --emitDeclarationOnly",
"build": "bun run build:js && bun run build:dts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "bun test"
}
}
The tsconfig.build.json extends the root config and narrows
to "emitDeclarationOnly": true, "declaration": true,
"outDir": "./dist", with "noEmit": false overridden. The
root tsconfig.json keeps "noEmit": true so the editor and
bun run typecheck agree on the same diagnostics.
If you have already turned on
--isolatedDeclarations
(and you should), you can replace stage 2 with
oxc's declaration transform
and run it in parallel per file. The win compounds on a
monorepo. The shape of the pipeline doesn't change. Bun still
doesn't emit declarations. You still need a separate
declaration step. The only question is whether that step is
tsc or a parallel friend.
The CI matrix that gets you both
The reason a single Bun job is not enough is the same reason
the runtime is fast: it does not type-check. The reason a
single tsc job is not enough is that tsc does not run your
code. You want both, and you want them in parallel.
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- run: bun install --frozen-lockfile
- run: bun test
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- run: bun install --frozen-lockfile
- run: bunx tsc --noEmit
build:
needs: [test, typecheck]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- run: bun install --frozen-lockfile
- run: bun run build
Two parallel jobs at the top, one for runtime correctness
(bun test), one for type correctness (tsc --noEmit). They
race. On a small library, both finish in well under a minute.
Type errors and runtime errors surface together. The build
job runs only after both pass.
For a library that needs to work on Node and Bun, add a
matrix dimension to the test job:
test:
strategy:
matrix:
runtime: [node, bun]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- if: matrix.runtime == 'node'
uses: actions/setup-node@v4
with: { node-version: '22' }
- run: bun install --frozen-lockfile
- if: matrix.runtime == 'bun'
run: bun test
- if: matrix.runtime == 'node'
run: node --test
The cost is a second test job. The win is that you find out
the moment your code accidentally relies on a Bun-only
behavior — a Bun.file() call that slipped in, a require
shape Bun forgives and Node does not, an ESM/CJS edge case.
For a library, that signal is worth the runner minutes.
The typecheck job is the one that does the work bun test
will not do. It runs tsc --noEmit against the same
tsconfig.json your editor uses. If a contributor opens a
PR that returns Promise<User> where the caller expects
User, this job catches it. If the same PR's tests happen to
not exercise the broken path (because the team is using
hand-rolled mocks, or the assertion is loose), bun test
will not catch it. The second job exists for that case.
When Bun replaces tsc-watch entirely
For an application — not a library — that runs only on Bun in
production, the answer is simpler. You replace tsc-watch and
keep one parallel bunx tsc --noEmit running in another
terminal (or as a pre-commit hook, or as a CI gate). The dev
loop is bun --hot run src/index.ts. The type-check loop is
the editor's TypeScript server plus a periodic tsc --noEmit.
There is no compile step in between.
For a library — anything that publishes .d.ts and runs in
runtimes you don't control — Bun replaces tsc for execution
and bundling, but not for declaration emit and not for the
strict cross-runtime contract. You still want tsc (or an
isolated-declarations equivalent) on the publish path.
For a monorepo with project references, the tsc -b --noEmit
job in CI is the only thing keeping cross-package type
correctness honest. Bun does not look at project references.
The references graph is what tells you a change in package A
is type-incompatible with package B before the broken
publish lands. Keep that job.
The rule that fits on a sticky note: Bun runs your
TypeScript. tsc checks it.
If this was useful
The CI shape above — Bun for the dev loop and the bundler,
tsc for the type gate and the declarations — is one of the
build-pipeline patterns TypeScript in Production covers
alongside dual ESM/CJS publishing, project references,
--isolatedDeclarations, and the JSR/Node/Bun publishing
dance. If you ship a TypeScript package and you've been
nudging the build off tsc toward Bun, that's the volume in
the set with the runtime-by-runtime breakdown.
For the language itself — types, narrowing, modules, async,
the daily-driver tooling — TypeScript Essentials is the
entry point. The TypeScript Type System picks up where
Essentials ends with the generics, mapped types, and infer
patterns library APIs are built from. From JVM, Kotlin and
Java to TypeScript makes the bridge; from PHP 8+, PHP to
TypeScript covers the same ground from the other side.
The five-book set:
- TypeScript Essentials — From Working Developer to Confident TS, Across Node, Bun, Deno, and the Browser — entry point: amazon.com/dp/B0GZB7QRW3
- The TypeScript Type System — From Generics to DSL-Level Types — deep dive: amazon.com/dp/B0GZB86QYW
- Kotlin and Java to TypeScript — A Bridge for JVM Developers — bridge for JVM devs: amazon.com/dp/B0GZB2333H
- PHP to TypeScript — A Bridge for Modern PHP 8+ Developers — bridge for PHP devs: amazon.com/dp/B0GZBD5HMF
- TypeScript in Production — Tooling, Build, and Library Authoring Across Runtimes — production layer: amazon.com/dp/B0GZB7F471
All five books ship in ebook, paperback, and hardcover.















