TypeScript with Deno: Native Support Without Any Build Step
AI generated
type
TypeScript
TypeScript with Deno
Native support without a build step

Deno runs .ts files directly, no tsc, no ts-node, no node_modules. Developers coming from the Node.js ecosystem need to recalibrate a few assumptions about type checking, modules, and security.

9 min read Deno Runtime

1. Why Deno runs TypeScript without any detour

Deno was built by Ryan Dahl, the original creator of Node.js, with the explicit realization that a large share of server-side JavaScript is now written in TypeScript. Instead of treating TypeScript as a bolted-on package, the transpiler is a core part of the runtime itself.

Any file ending in .ts, .tsx, or .mts is automatically recognized at execution time and started without a separate compile step. There is no required tsconfig.json, no ts-node loader, and no transpiler registration on the command line.

This lowers the barrier to entry for new projects noticeably, but it also shifts responsibility: developers who want real type checking before shipping have to plan it as an explicit step, since deno run does not do it by default.

For teams coming from a Node.js setup built on ts-node or tsx, this removes an entire category of configuration headaches: loader registration, path resolution, and ESM/CommonJS interop are solved consistently from the start in Deno instead of being pieced together per project.

2. Getting started: deno run without any setup

A new script starts without npm init, without a package.json, and without an installation step. A single file with typed functions can run directly via deno run, complete with editor autocompletion once the Deno language server is active.

Permissions are granted explicitly through flags, such as --allow-net for network access or --allow-read for filesystem access. Without these flags, any matching call fails at runtime with a clear error message, not silently later at deployment.

For everyday use, deno run --allow-net=api.example.com script.ts is enough to start a typed script with restricted network access, with no prior project scaffolding required.


// greet.ts
interface Greeting {
  name: string;
  loud?: boolean;
}

function greet({ name, loud = false }: Greeting): string {
  const message = `Hello, ${name}!`;
  return loud ? message.toUpperCase() : message;
}

console.log(greet({ name: "team", loud: true }));

// Run: deno run greet.ts

3. Under the hood: type stripping, not type checking

A plain deno run only strips the type annotations out of the source, similar to the experimental strip mode Node.js shipped starting with version 22. No semantic validation happens at this stage: incorrect types are simply discarded.

For full type checking there is a separate command, deno check, which loads the actual TypeScript compiler core and reports every error tsc --noEmit would also catch. That step belongs in every CI pipeline, but is deliberately skipped locally to keep startup fast.

This split is the biggest difference from a classic ts-node setup: fast iteration during development, with an explicit, separate validation step before merging or releasing.

In practice, wiring deno check into a pre-commit hook or a dedicated CI job before the test run pays off, so type errors surface before production instead of when a mistyped function gets called with unexpected values.

4. Import maps and URL imports instead of node_modules

By default Deno imports modules through full URLs or through npm: and jsr: specifiers, rather than expecting a local node_modules folder. Downloaded modules land in a global, content-addressed cache and are shared across projects.

Import maps in deno.json translate short identifiers into full specifiers, so imports in the code stay readable instead of repeating long URLs in every file.

For existing npm packages that are not ESM-native, the npm: specifier usually works transparently, including CommonJS interop resolution behind the scenes.


// deno.json (excerpt)
{
  "imports": {
    "std/": "https://deno.land/std@0.224.0/",
    "zod": "npm:zod@^3.23.0"
  }
}

// main.ts
import { z } from "zod";
import { parse } from "std/csv/mod.ts";

const Schema = z.object({ id: z.number(), name: z.string() });

5. deno.json and deno.jsonc: one central config file

Instead of maintaining package.json, tsconfig.json, and .eslintrc separately, deno.json bundles compiler options, import maps, lint rules, formatting settings, and named tasks in one file.

The compilerOptions section supports most of the fields known from tsconfig.json, such as strict or lib, but it only affects deno check, not execution itself.

Tasks under tasks replace npm scripts: deno task dev runs defined commands just like npm run dev, without needing an extra tool.


// deno.json
{
  "compilerOptions": {
    "strict": true,
    "lib": ["deno.window"]
  },
  "tasks": {
    "dev": "deno run --watch --allow-net main.ts",
    "test": "deno test --allow-read"
  }
}

6. The permissions model and what it means for TypeScript code

Deno starts every process with no access to the filesystem, network, environment variables, or subprocesses by default. Every extension must be granted explicitly as a flag, which enforces least-privilege at the runtime level itself.

For TypeScript libraries this means type definitions alone say nothing about actual side effects: a function can be perfectly type-correct and still fail at call time due to missing permissions if the flags are not set.

In practice it pays to scope permissions as narrowly as possible, for example --allow-read=./config instead of a blanket --allow-read, to rule out accidental access to sensitive paths.

7. Testing with Deno.test and TypeScript

The built-in test runner deno test needs no extra library like Jest or Vitest. Assertions come from the standard library under std/assert and are already fully typed.

Test files are typically named *_test.ts or placed under tests/ and get picked up automatically. Snapshot tests, coverage reports via deno test --coverage, and parallel execution are all available with no extra config.

Because test files are also subject to the permissions model, tests that simulate network or filesystem access can be isolated deliberately with the matching flags.


// greet_test.ts
import { assertEquals } from "std/assert/mod.ts";
import { greet } from "./greet.ts";

Deno.test("greet formats name correctly", () => {
  assertEquals(greet({ name: "team" }), "Hello, team!");
});

Deno.test("greet respects loud flag", () => {
  assertEquals(greet({ name: "team", loud: true }), "HELLO, TEAM!");
});

8. From deno compile to Deno Deploy: shipping TypeScript

deno compile bundles a TypeScript program together with the runtime into a single executable for the target platform, with no separate Deno installation required on that machine. This is a good fit for CLI tools distributed as a binary.

For web services, Deno Deploy provides an edge runtime that runs TypeScript handlers directly from the repository, including automatic scaling across globally distributed regions.

Both paths share the same type-stripping mechanism as deno run, which is why a prior deno check run in the CI pipeline before deployment remains strongly recommended.


# Compiles main.ts into a standalone executable
deno compile --allow-net --output my-tool main.ts

./my-tool

9. Migrating from Node.js: npm specifiers and interop

A full rewrite is rarely required for a migration. The npm: specifier lets you import existing npm dependencies directly while internal project code moves gradually toward native Deno APIs.

Node-specific APIs like fs, path, or process are available through the node: prefix, which considerably simplifies porting Node.js scripts without rewriting every single line.

Going the other way, Node.js has experimentally supported running TypeScript files directly via type stripping since version 22, which is steadily narrowing the gap between the two runtimes for plain TS execution.


// Using Node APIs and npm packages together
import { readFileSync } from "node:fs";
import { z } from "npm:zod@^3.23.0";

const raw = readFileSync("./config.json", "utf-8");
const Config = z.object({ port: z.number() });
const config = Config.parse(JSON.parse(raw));
Aspect Deno Node.js + ts-node Bun
TypeScript support Native, no extra package Only via ts-node/tsx as an extra package Native, own transpiler
Type checking on startup Off by default, separate deno check Depends on ts-node configuration Off by default, no built-in checker
Package management URL/npm imports, no node_modules required npm with node_modules npm-compatible, own installer
Security model Explicit permissions (--allow-read etc.) No built-in sandboxing No built-in sandboxing
Compiling to a binary deno compile produces a single executable Not native, requires external tools bun build --compile available

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

TypeScript with Deno

No build step

Type stripping directly at runtime

Security sandbox

Explicit permissions, not full access

npm compatibility

npm: specifier for existing packages

Single binary

deno compile for standalone executables

11. FAQ: TypeScript with Deno

1Does Deno need a tsconfig.json?
No. Deno works without any config file at all. Compiler options can optionally be set in the compilerOptions block of deno.json, which then applies to deno check.
2Does deno run actually check the types?
No, deno run only strips type annotations without validating them. Real type checking runs exclusively through the separate deno check command.
3Can I use npm packages in Deno?
Yes, the npm: specifier lets you import most npm packages directly, including automatic CommonJS interop resolution behind the scenes.
4What is the difference between deno.json and package.json?
deno.json bundles compiler options, import maps, lint and format rules, and tasks into one file, while package.json in Node.js primarily manages dependencies and scripts.
5How does the permissions model interact with TypeScript libraries?
Type definitions say nothing about actual side effects. A function can be perfectly type-correct and still fail at runtime because a flag like --allow-net was missing.
6Can I distribute Deno code as a standalone executable?
Yes, deno compile bundles the program and runtime into a single executable that runs on the target platform without a separate Deno installation.
7How do I test TypeScript code with Deno without an extra library?
The built-in test runner deno test uses assertions from std/assert and automatically discovers files like *_test.ts, including coverage reports and parallel execution.
8Is migrating from Node.js to Deno worth it?
For existing projects, rarely as a full switch. A more realistic path is gradual adoption via the npm: specifier for new tools, CLIs, or edge functions.
9Does Deno support JSX and TSX files?
Yes, .tsx files are recognized and transpiled with no extra configuration, and the JSX factory can be adjusted via compilerOptions in deno.json.
10Can Node.js now run TypeScript directly too?
Since Node.js 22 there is an experimental type-stripping mode that removes type annotations similarly to Deno, but without a built-in type checker or permissions model.