Deno's Built-in TypeScript: What Happens When You Skip the Build Step
Deno runs TypeScript directly by embedding the compiler and using snapshots for fast startup. The catch: execution skips type checking, so you need a separate `deno check` step.
08 Oct 2026, 23:48 UTC

The Problem: TypeScript's Compilation Tax
Every TypeScript project traditionally starts with a build pipeline: tsconfig.json, a bundler or tsc watch mode, source maps, and a dist folder. That pipeline exists because browsers and Node.js only understand JavaScript. The compilation step adds latency to every iteration, configuration surface area, and a class of bugs where the running code diverges from the source.
Deno takes a different bet: ship the TypeScript compiler inside the runtime. When you run deno run server.ts, the runtime parses, type-strips, and executes your .ts file directly. No node_modules, no package.json, no separate tsc invocation. This article walks through how that works, where it saves time, and the trade-offs you accept when you remove the build step.
How Deno Executes TypeScript Without a Build
Deno embeds a snapshot of the TypeScript compiler into the runtime binary. On first run of a given script, Deno follows these steps:
- Fetches and caches any remote imports (URL-based, ES Modules only).
- Runs the embedded compiler in transpile-only mode: it strips type annotations, transforms newer ECMAScript features, and emits JavaScript into an internal cache.
- Executes the resulting JavaScript via the V8 engine.
The snapshotting mechanism is the key to startup performance. Deno serializes the initialized TypeScript compiler state into a binary snapshot at build time. At runtime, it deserializes that snapshot instead of re-initializing the compiler from scratch, reducing the cold-start penalty.
Because the compiler runs in transpile-only mode, no type checking occurs during deno run. Type errors that would block tsc are ignored; the code executes as long as it is syntactically valid after type erasure. This is a deliberate performance decision: type checking is a whole-program analysis that would significantly slow down per-file execution.
Worked Example: From Zero to Running Server
Create a file named hello.ts anywhere on your filesystem:
// hello.ts
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
const handler = (req: Request): Response => {
return new Response("Hello from Deno + TypeScript\n");
};
console.log("Listening on http://localhost:8000");
await serve(handler, { port: 8000 });
Run it from a terminal (no admin rights required):
deno run --allow-net hello.ts
What happens:
- Deno downloads the standard library module and caches it locally.
- The embedded compiler strips the
: Requestand: Responseannotations. - V8 executes the server. The
--allow-netflag grants the process permission to bind to port 8000; without it, Deno throws aPermissionDeniederror.
Verification: Open http://localhost:8000 in a browser or run curl http://localhost:8000. You should see the plain text response. Stop the server with Ctrl-C.
Risk note: The --allow-net flag opens all outbound and inbound network access. In production, restrict it to specific hosts: --allow-net=localhost:8000.
The Trade-off: Explicit Type Checking Is a Separate Step
Because deno run skips type checking, you can ship code with type errors that only surface at runtime. Deno provides deno check for CI and pre-commit hooks:
deno check hello.ts
This runs the full type checker across the module graph and exits non-zero on errors. It does not execute code. A typical workflow looks like this:
- Development:
deno run --watch --allow-net hello.tsfor fast iteration. - Pre-push / CI:
deno check **/*.tsfollowed bydeno test --allow-net.
This separation mirrors the "inner loop vs. outer loop" distinction: fast unchecked runs while coding, rigorous checking before merge.
Limitations and Compatibility Edges
- CommonJS interop: Deno is ES Modules only. Importing a legacy CommonJS package requires the
npm:specifier (e.g.,import express from "npm:express@4"), which uses a compatibility layer. - Version drift: The embedded TypeScript version is tied to the Deno release. You cannot independently upgrade the compiler without upgrading Deno.
- Declaration files:
deno rundoes not emit.d.tsfiles. Library authors must use a separate packaging step to emit declarations.
How to Verify This Behavior Yourself
- Install Deno via the official installer or package manager.
- Create a file with an intentional type error:
const x: number = "oops"; console.log(x);. - Run
deno run bad.ts— it prints "oops" because types are stripped. - Run
deno check bad.ts— it reports the type mismatch error. - Remove
--allow-netfrom the server example to observe thePermissionDeniederror.
Closing: When the Build Step Disappears
Removing the build step shifts complexity from configuration to runtime guarantees. You trade tsconfig.json tuning for a fixed compiler version and URL import caching. For scripts, CLIs, and services where iteration speed matters, Deno's model reduces friction without removing the safety net—it simply moves the net to deno check.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.