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.
Table of Contents
- 1. Why Deno runs TypeScript without any detour
- 2. Getting started: deno run without any setup
- 3. Under the hood: type stripping, not type checking
- 4. Import maps and URL imports instead of node_modules
- 5. deno.json and deno.jsonc: one central config file
- 6. The permissions model and what it means for TypeScript code
- 7. Testing with Deno.test and TypeScript
- 8. From deno compile to Deno Deploy: shipping TypeScript
- 9. Migrating from Node.js: npm specifiers and interop
- 10. Summary
- 11. FAQ
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