TypeScript 92 ๐ท TypeScript with Deno and Bun
The previous chapter covered Node.js and Express โ the traditional backend stack, where TypeScript is a layer added on top of a runtime that was designed for JavaScript. Deno and Bun approach TypeScript differently. Both runtimes were built from the ground up with TypeScript as a first-class language, not an afterthought. Neither requires tsc to execute code. Both strip types at runtime and execute the result. The difference is in what they do with the types after stripping โ and what they expect you to do for type checking.
Deno’s philosophy is security and standards compliance. It treats TypeScript as a first-class language, compiles it internally with its own embedded TypeScript compiler, and type-checks in strict mode by default . Bun’s philosophy is speed and compatibility. It strips types using its own transpiler, runs TypeScript natively, and positions itself as a drop-in replacement for Node.js with a focus on performance .
Key point: Neither Deno nor Bun type-checks your code at runtime. Type stripping is a transformation, not a validation. Deno provides deno check and Bun expects you to run tsc --noEmit or bunx tsc --noEmit separately . The type annotations you write are erased before execution. The runtime does not know what types you intended, and it will not tell you if your code violates them.
Why Deno and Bun matter for TypeScript
Node.js was designed for JavaScript, and TypeScript support was retrofitted through external tools. Deno and Bun were designed after TypeScript became the dominant language for serious JavaScript development. That timing shapes their architecture.
The build-step elimination. In a Node.js TypeScript project, you either compile with tsc before running, or you use a tool like ts-node or tsx. Both add a step between writing the code and running it. Deno and Bun eliminate that step. deno run main.ts and bun run main.ts both execute the TypeScript file directly. The runtime strips the types and runs the result .
The security model. Deno was designed with security as a first principle. JavaScript in Node.js has unrestricted access to the filesystem, network, and environment. Deno requires explicit permission for each of these. This affects how you write backend code, and it affects how TypeScript is configured. Deno’s type definitions include the permission model, so the compiler knows which APIs require flags .
The performance model. Bun is built on JavaScriptCore, the engine that powers Safari, and uses Zig for its runtime implementation. Its TypeScript transpiler is written in Zig and is significantly faster than tsc or swc. For projects where startup time and execution speed matter โ serverless functions, CLI tools, test suites โ Bun’s performance advantage is measurable .
The trade-off. Deno and Bun both sacrifice some Node.js compatibility for their respective advantages. Deno’s permission model means existing Node.js code that reads files without flags will fail. Bun’s focus on speed means some edge cases in Node.js’s module resolution may behave differently. Both have npm: and node: compatibility layers, but neither is a perfect drop-in for every project.
a. TypeScript in Deno
Deno treats TypeScript as a first-class language, the same as JavaScript or WebAssembly. You can run or import TypeScript without installing anything beyond the Deno CLI. Deno includes its own TypeScript compiler, built into the runtime, which transpiles TypeScript to JavaScript and stores the result in a cache .
The type-checking behavior is the first thing to understand. Deno type-checks in strict mode by default, and it also enables noImplicitOverride, which tsc leaves off even under strict . But type checking is not performed before execution by default. deno run main.ts transpiles the file and runs it without checking types. To check types before running, you use deno run --check main.ts or deno check main.ts .
// main.ts
function greet(name: string): string {
return `Hello, ${name}`;
}
console.log(greet("Deno"));
Running deno run main.ts executes the code. Running deno check main.ts checks the types. Running deno run --check main.ts does both, and exits before execution if a type error is found.
Deno’s configuration lives in deno.json rather than tsconfig.json for Deno-first projects. The compilerOptions section accepts a subset of TypeScript’s options. Deno’s defaults are strict and modern, so most projects need no configuration at all . The options that Deno ignores are those that shape emitted JavaScript โ target, module, outDir, sourceMap โ because Deno does not write JavaScript files to disk. It keeps the transpiled output in its internal cache .
The lib setting is where Deno diverges from standard TypeScript. Deno’s default lib is ["deno.window"], which includes the Deno global namespace and the web platform APIs that Deno implements . If you want code that also runs in the browser, you add "dom" and "dom.iterable" to the lib array, and use "deno.ns" instead of "deno.window" to avoid conflicts between the two libraries’ global declarations .
// deno.json
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
}
}
This configuration allows a single file to be type-checked against both the browser and Deno environments. The deno.ns library provides the Deno global without the full deno.window library, avoiding conflicts with dom.
Deno’s module resolution is also different from tsc. Deno uses explicit specifiers โ import { foo } from "./foo.ts" โ rather than tsc‘s ambiguous module resolution. When consuming JavaScript that has an associated .d.ts file, Deno does not automatically pick up the .d.ts the way tsc does. You must explicitly specify where the types are located .
b. TypeScript in Bun
Bun executes TypeScript natively without a separate compilation step. Its runtime includes a built-in TypeScript transpiler written in Zig. When you run a .ts or .tsx file, Bun strips the types on the fly and executes the resulting JavaScript in the same process, with no intermediate files written to disk .
Bun’s type stripping is similar to Node.js’s, but with broader support. Bun handles all TypeScript syntax, including enum, namespaces with runtime values, and parameter properties, without requiring experimental flags . The ts loader “strips out all TypeScript syntax, then behaves identically to the js loader” . Bun does not perform type checking.
// index.ts
const server = Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun");
},
});
console.log(`Listening on ${server.url}`);
Running bun run index.ts starts the server immediately. No build step, no tsc, no configuration.
The TypeScript configuration for Bun is also different from a Node.js project. Bun recommends a specific tsconfig.json that enables features TypeScript does not allow by default: top-level await, importing .ts files by extension, and JSX . The key settings are:
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true
}
}
The "types": ["bun"] entry is required for TypeScript 6.0 and later, which no longer auto-discovers @types/* packages . The "moduleResolution": "bundler" setting matches Bun’s resolution algorithm, which allows importing .ts files directly. The "noEmit": true setting tells TypeScript that Bun handles execution, so TypeScript should only be used for type checking .
Bun reads paths from tsconfig.json and uses them for runtime module resolution. Path aliases configured for TypeScript work at runtime without a separate bundler configuration . Bun also supports importing JSON, TOML, YAML, and other file types directly, with types provided by @types/bun .
Type checking in Bun is entirely separate from execution. bunx tsc --noEmit runs the TypeScript compiler in check-only mode. If TypeScript is installed as a dev dependency, bun run tsc --noEmit works. The recommended package.json script is "typecheck": "tsc --noEmit" .
c. Choosing Between Deno and Bun
Both runtimes eliminate the build step for TypeScript, but they target different use cases.
Deno for security-sensitive and standards-compliant projects. Deno’s permission model means that code cannot access the filesystem, network, or environment without explicit flags. This is valuable for running untrusted code, for CLI tools that should be transparent about what they access, and for projects where the principle of least privilege matters. Deno’s strict-by-default type checking and its alignment with web standards make it a good choice for code that also runs in the browser .
Bun for performance-sensitive and Node.js-compatible projects. Bun’s JavaScriptCore engine and Zig-based runtime give it a measurable speed advantage over Node.js for many workloads. Its compatibility with Node.js’s module resolution, require(), and npm packages means existing Node.js projects can often run under Bun with minimal changes. The built-in bundler, test runner, and package manager make Bun a complete toolkit rather than just a runtime .
The shared limitation. Neither runtime type-checks at execution time. Deno provides deno check and deno run --check. Bun expects tsc --noEmit. In both cases, the type checking is a separate step that must be run in CI or in an editor that understands TypeScript. The runtime will happily execute code with type errors, just as Node.js executes JavaScript with logical errors.
The migration cost. Moving an existing Node.js project to Deno requires adapting to the permission model and the explicit module resolution. Moving to Bun is generally easier because Bun implements the Node.js module resolution algorithm and supports both require() and import . For new projects, the choice depends on whether security (Deno) or speed and compatibility (Bun) is the priority.
Complete Example Session
This session builds a small HTTP server and a CLI tool in both Deno and Bun, demonstrating the runtime-specific type definitions and the type-checking workflow.
// ============================================
// PART 1: THE DENO HTTP SERVER
// ============================================
// deno-server.ts
Deno.serve({ port: 8000 }, (request: Request): Response => {
const url = new URL(request.url);
return new Response(`Path: ${url.pathname}`, {
headers: { "content-type": "text/plain" },
});
});
// Run: deno run --allow-net deno-server.ts
// Check: deno check deno-server.ts
// ============================================
// PART 2: THE DENO CLI TOOL
// ============================================
// deno-cli.ts
const args = Deno.args;
if (args.length === 0) {
console.error("Usage: deno run --allow-read deno-cli.ts <file>");
Deno.exit(1);
}
const filename = args[0];
try {
const content = await Deno.readTextFile(filename);
const lines = content.split("\n").length;
console.log(`${filename}: ${lines} lines`);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.error(`File not found: ${filename}`);
Deno.exit(2);
}
throw error;
}
// Run: deno run --allow-read deno-cli.ts package.json
// The Deno.errors.NotFound type is available from deno.ns.
// ============================================
// PART 3: THE BUN HTTP SERVER
// ============================================
// bun-server.ts
const server = Bun.serve({
port: 3000,
fetch(request: Request): Response {
const url = new URL(request.url);
return new Response(`Path: ${url.pathname}`, {
headers: { "content-type": "text/plain" },
});
},
});
console.log(`Server running at ${server.url}`);
// Run: bun run bun-server.ts
// The Bun.serve types are provided by @types/bun.
// ============================================
// PART 4: THE BUN CLI TOOL
// ============================================
// bun-cli.ts
const args = Bun.argv.slice(2);
if (args.length === 0) {
console.error("Usage: bun run bun-cli.ts <file>");
process.exit(1);
}
const filename = args[0];
const file = Bun.file(filename);
if (!(await file.exists())) {
console.error(`File not found: ${filename}`);
process.exit(2);
}
const content = await file.text();
const lines = content.split("\n").length;
console.log(`${filename}: ${lines} lines`);
// Run: bun run bun-cli.ts package.json
// Bun.file returns a BunFile with exists() and text() methods.
// ============================================
// PART 5: THE DENO CONFIGURATION
// ============================================
// deno.json
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"tasks": {
"check": "deno check **/*.ts",
"run:server": "deno run --allow-net deno-server.ts"
}
}
// Run: deno task check
// Run: deno task run:server
// ============================================
// PART 6: THE BUN CONFIGURATION
// ============================================
// tsconfig.json
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"allowJs": true,
"types": ["bun"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true
}
}
// package.json
{
"scripts": {
"typecheck": "tsc --noEmit",
"start": "bun run bun-server.ts"
}
}
// Run: bun run typecheck
// Run: bun run start
// ============================================
// PART 7: THE DENO TYPE CHECKING
// ============================================
// Check without running
deno check deno-server.ts
// Check and run
deno run --check --allow-net deno-server.ts
// Check all TypeScript files
deno check **/*.ts
// Type checking is NOT run by default with deno run.
// The --check flag enables it.
// ============================================
// PART 8: THE BUN TYPE CHECKING
// ============================================
// Bun does not type-check at all.
// Use tsc directly:
bunx tsc --noEmit
// Or if TypeScript is a dev dependency:
bun run tsc --noEmit
// Add to package.json scripts for CI:
// "typecheck": "tsc --noEmit"
// ============================================
// PART 9: THE DENO TEST RUNNER
// ============================================
// deno-test.ts
import { assertEquals } from "jsr:@std/assert";
function add(a: number, b: number): number {
return a + b;
}
Deno.test("add returns sum", () => {
assertEquals(add(2, 3), 5);
});
// Run: deno test
// Type checking is enabled by default during tests.
// Skip with: deno test --no-check
// ============================================
// PART 10: THE BUN TEST RUNNER
// ============================================
// bun-test.test.ts
import { expect, test } from "bun:test";
function add(a: number, b: number): number {
return a + b;
}
test("add returns sum", () => {
expect(add(2, 3)).toBe(5);
});
// Run: bun test
// Bun's test runner does not type-check.
// Use tsc --noEmit separately.
The ten parts cover the Deno HTTP server, the Deno CLI tool, the Bun HTTP server, the Bun CLI tool, the Deno configuration, the Bun configuration, Deno type checking, Bun type checking, the Deno test runner, and the Bun test runner.
Quick Reference
The Runtime Comparison
| Feature | Deno | Bun |
|---|---|---|
| TypeScript execution | Native | Native |
| Type checking | deno check | tsc --noEmit |
| Check before run | deno run --check | Not built-in |
| Strict mode default | โ Yes | Depends on tsconfig.json |
| Config file | deno.json | tsconfig.json |
| Type definitions | Built-in (deno.ns) | @types/bun |
| Module resolution | Explicit specifiers | Node.js algorithm |
| Permission model | โ Yes | โ No |
enum support | โ Yes | โ Yes |
The Deno Type-Checking Commands
| Command | Purpose |
|---|---|
deno check main.ts | Type-check without running |
deno check --all main.ts | Check remote modules and npm |
deno run --check main.ts | Check before running |
deno test | Type-checking enabled by default |
deno test --no-check | Skip type checking in tests |
The Bun Configuration Settings
| Setting | Value | Purpose |
|---|---|---|
lib | ["ESNext"] | Latest JavaScript features |
target | "ESNext" | Output target |
module | "Preserve" | Bun handles import/export |
moduleResolution | "bundler" | Matches Bun’s resolution |
allowImportingTsExtensions | true | Import .ts files directly |
noEmit | true | TypeScript only checks |
types | ["bun"] | Load Bun global types |
The Deno lib Settings
| Library | Purpose |
|---|---|
"deno.window" | Default Deno runtime library |
"deno.ns" | Deno global namespace only |
"deno.worker" | Web worker Deno library |
"dom" | Browser DOM (conflicts with deno.window) |
"dom.iterable" | DOM iterables |
Best Practices
โ Do This:
// Use deno check in CI
deno check **/*.ts // โ
// Use deno run --check for type-checked execution
deno run --check main.ts // โ
// Add "types": ["bun"] for TypeScript 6.0+
"types": ["bun"] // โ
// Run tsc --noEmit for Bun type checking
bunx tsc --noEmit // โ
// Use the recommended Bun tsconfig
"moduleResolution": "bundler" // โ
โ Don’t Do This:
// Don't assume Deno type-checks on run
deno run main.ts // does NOT type-check // โ
// Don't assume Bun type-checks at all
bun run main.ts // no type checking // โ
// Don't forget @types/bun
// Without it, Bun global is untyped. // โ
// Don't use dom with deno.window
"lib": ["deno.window", "dom"] // conflicts // โ
// Don't rely on Deno permissions in Bun
// Bun has no permission model. // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Deno runs code with type errors | Type checking not enabled by default | Use deno run --check |
| Bun runs code with type errors | Bun never type-checks | Run tsc --noEmit separately |
Cannot find name 'Bun' | Missing @types/bun or types config | Install @types/bun, add "types": ["bun"] |
Cannot find name 'Deno' | Missing deno.ns in lib | Add "deno.ns" to lib |
dom conflicts with deno.window | Both declare global types | Use "deno.ns" instead of "deno.window" |
.d.ts file not found | Deno does not auto-discover | Explicitly specify the path |
Import .ts fails in Bun | Missing allowImportingTsExtensions | Add to tsconfig.json |
Real-World Examples
1. Deno HTTP Server
Deno.serve({ port: 8000 }, (req) => new Response("Hello"));
2. Deno CLI Tool
const args = Deno.args;
if (args.length === 0) Deno.exit(1);
3. Deno File Read
const content = await Deno.readTextFile("file.txt");
4. Deno Check
deno check main.ts
5. Deno Run with Check
deno run --check main.ts
6. Bun HTTP Server
const server = Bun.serve({ port: 3000, fetch(req) { return new Response("Hello"); } });
7. Bun CLI Tool
const args = Bun.argv.slice(2);
8. Bun File Read
const file = Bun.file("file.txt");
const content = await file.text();
9. Bun Type Check
bunx tsc --noEmit
10. Deno Test
Deno.test("name", () => { assertEquals(1, 1); });
Visual
The TypeScript Execution Pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DENO EXECUTION โ
โ โ
โ main.ts โโ> [type strip] โโ> cache โโ> run โ
โ โ
โ Type checking is separate: โ
โ deno check main.ts โ
โ โ
โ Or combined: โ
โ deno run --check main.ts โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ BUN EXECUTION โ
โ โ
โ main.ts โโ> [type strip] โโ> run โ
โ โ
โ No cache, no type checking. โ
โ Type checking is entirely external: โ
โ bunx tsc --noEmit โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Deno lib Configuration
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DENO lib SETTINGS โ
โ โ
โ Default: โ
โ lib: ["deno.window"] โ
โ โโ Deno global + web APIs โ
โ โ
โ Deno + Browser: โ
โ lib: ["dom", "dom.iterable", "deno.ns"] โ
โ โโ Browser globals + Deno global โ
โ โโ deno.ns avoids conflict with dom โ
โ โ
โ โ Don't use deno.window + dom together โ
โ (conflicting global declarations) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Bun tsconfig Requirements
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ BUN tsconfig.json โ
โ โ
โ "lib": ["ESNext"] โ
โ "target": "ESNext" โ
โ "module": "Preserve" โ
โ "moduleResolution": "bundler" โ
โ "allowImportingTsExtensions": true โ
โ "verbatimModuleSyntax": true โ
โ "noEmit": true โ
โ "types": ["bun"] โ
โ โ
โ These settings match Bun's runtime. โ
โ "noEmit" tells TypeScript to only check. โ
โ "types" loads @types/bun for globals. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Type Checking Workflow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TYPE CHECKING WORKFLOW โ
โ โ
โ Deno: โ
โ deno check **/*.ts โ CI โ
โ deno run --check main โ development โ
โ deno test โ tests (auto) โ
โ โ
โ Bun: โ
โ bunx tsc --noEmit โ CI โ
โ bun run tsc --noEmit โ development โ
โ โ
โ Neither runtime checks types at run time. โ
โ Both require an explicit check command. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Deno TypeScript | Native, first-class, built-in compiler |
| Deno type checking | deno check, deno run --check |
| Deno config | deno.json with compilerOptions |
| Deno strict default | โ Yes |
| Deno lib default | ["deno.window"] |
| Bun TypeScript | Native, type-stripping transpiler |
| Bun type checking | tsc --noEmit (external) |
| Bun config | tsconfig.json with "types": ["bun"] |
| Bun types package | @types/bun |
| Bun module resolution | Node.js algorithm |
Key takeaways:
- Both Deno and Bun execute TypeScript natively without a build step. They strip type annotations at runtime and run the resulting JavaScript. Neither writes intermediate
.jsfiles to disk . - Neither runtime type-checks at execution time. Deno provides
deno checkanddeno run --checkfor explicit checking. Bun expectstsc --noEmitas a separate step. Type checking is never automatic . - Deno’s type checking is strict by default. It enables
strictandnoImplicitOverride, whichtscleaves off even under strict mode. Deno’s configuration lives indeno.json. - Deno’s
libsettings differ from standard TypeScript. The default is["deno.window"], which includes the Deno global namespace and web APIs. For browser-compatible code, use["dom", "dom.iterable", "deno.ns"]instead . - Bun requires
"types": ["bun"]intsconfig.jsonfor TypeScript 6.0+. Starting with TypeScript 6.0, thetypesfield defaults to an empty array, so@types/bunmust be explicitly listed. The recommendedtsconfig.jsonalso sets"moduleResolution": "bundler","allowImportingTsExtensions": true, and"noEmit": true. - Deno has a permission model; Bun does not. Deno requires explicit flags for filesystem, network, and environment access. Bun follows Node.js’s unrestricted model. This affects both runtime behavior and the type definitions for permission-gated APIs .
- Bun implements the Node.js module resolution algorithm. It supports both
require()andimport, and readspathsfromtsconfig.jsonfor runtime resolution. This makes migration from Node.js easier than migration to Deno .
Remember: Deno and Bun both treat TypeScript as a first-class language, but neither treats it as a type-checked language. The type annotations are erased before execution. Deno gives you deno check and strict-by-default configuration. Bun gives you speed, Node.js compatibility, and a recommended tsconfig.json that expects external type checking. The discipline is the same in both cases: write TypeScript, run it directly, and check the types as a separate step in CI. The runtime will not tell you if the types are wrong. Only the compiler will.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!