Essential Deno TypeScript practices for ALL Deno development: configuration, imports, testing, permissions, and anti-patterns.
Use this skill for ALL Deno TypeScript development:
node_modules at all costs - reduce supply chain attack surfacetsconfig.json - Deno uses deno.json(c) as the single source of truthdeno check / deno test - do not rely on external tscDeno.*, Web APIs, Fetch, URLPattern) over polyfillsAlways use the strictest possible settings in deno.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}
Use deno.json or deno.jsonc as the single configuration file for:
Complete Configuration Example:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"tasks": {
"dev": "deno run --watch --allow-net --allow-read --allow-env src/main.ts",
"test": "deno test --allow-net --allow-read --allow-env --coverage=coverage src/",
"test:unit": "deno test --allow-net --allow-read --allow-env --coverage=coverage src/",
"test:e2e": "deno test --allow-net --allow-read --allow-env tests/e2e/",
"test:watch": "deno test --allow-net --allow-read --allow-env --watch --fail-fast",
"coverage": "deno coverage coverage --html",
"check": "deno check $(find src -name '*.ts' -not -name '*.sql')",
"lint": "deno lint",
"fmt": "deno fmt"
},
"imports": {
"@/": "./src/",
"@/domain/": "./src/domain/",
"@/infrastructure/": "./src/infrastructure/",
"@/application/": "./src/application/",
"@std/assert": "jsr:@std/assert@^1.0.14",
"@std/fs": "jsr:@std/fs@^1.0.19",
"@std/testing": "jsr:@std/testing@^1.0.15",
"@std/ulid": "jsr:@std/ulid@1",
"zod": "npm:zod@^3.23.8"
},
"exclude": [
"coverage/",
"node_modules/"
],
"lock": true
}
Define these deno task aliases at minimum:
dev - Development with watch modetest, test:watch - Testingcoverage - Generate coverage reportscheck - Type-check all fileslint, fmt - Code qualitydeno.lock to version control--lock=deno.lock --lock-write=false in CIdeno cache --lock=deno.lock --lock-writeCRITICAL: Never use direct JSR/npm imports in source files. All external dependencies MUST be declared in deno.json import map.
Import Order in Source Files:
// 1. Standard library imports (via import map)
import { assertEquals } from "@std/assert";
// 2. Third-party imports (via import map)
import { z } from "zod";
// 3. Internal imports (absolute paths using import map)
import { Agent } from "@/domain/agent.ts";
// 4. Relative imports (only within same module/context)
import { validatePrompt } from "./validation.ts";
Use sources in this order:
jsr: registry (first choice for TypeScript modules)
"@std/assert": "jsr:@std/assert@^1.0.14"
npm: specifier (when needed; prefer ESM-compatible)
"zod": "npm:zod@^3.23.8"
URL imports (rarely needed with import maps)
CRITICAL: Version pin all external imports. No floating @latest in committed code.
{
"imports": {
"zod": "npm:zod@^3.23.8", // GOOD - pinned
"zod": "npm:zod", // BAD - no version
"@std/assert": "jsr:@std/assert@1" // GOOD - pinned
}
}
Use import map aliases for clean internal imports:
{
"imports": {
"@/": "./src/",
"@/domain/": "./src/domain/",
"@/infrastructure/": "./src/infrastructure/"
}
}
// GOOD - Clean, refactor-safe
import { Agent } from "@/domain/agent.ts";
// BAD - Brittle relative paths
import { Agent } from "../../../domain/agent.ts";
Use type-only imports when importing types:
import type { Agent } from "@/domain/agent.ts";
import type { z } from "zod";
Unit Tests - Co-located with Source:
src/
└── domain/
├── agent.ts
└── agent.test.ts # Unit tests next to code
Integration & E2E Tests - Separate Directory:
tests/
├── integration/
│ └── openai_provider.test.ts
└── e2e/
└── workflow.test.ts
Why Co-location:
deno test discoveryNon-Negotiable:
deno test --coverage=coverage
deno coverage coverage --html
Always use explicit AAA (Arrange-Act-Assert):
import { assertEquals } from "@std/assert";
Deno.test("agent should process valid input", () => {
// Arrange
const agent = new Agent({ name: "TestAgent" });
const input = "Hello, world!";
// Act
const result = agent.process(input);
// Assert
assertEquals(result.status, "success");
});
Red-Green-Refactor with fast feedback:
# Watch mode with fail-fast
deno test --watch --fail-fast
# Run specific file
deno test src/domain/agent.test.ts --watch
CRITICAL: All tests must be deterministic.
Test Flakiness Policy:
Use stable seeds and fixtures:
import { FakeTime } from "@std/testing/time";
Deno.test("timer test", () => {
using time = new FakeTime();
// Deterministic time control
time.tick(1000);
});
All test files must end with .test.ts:
agent.test.ts # GOOD
agent_test.ts # BAD
agent.spec.ts # BAD
@std/assert for assertions@std/testing/mock for test doubles@std/testing/time for time controlDefault to minimum required permissions:
# BAD
deno run --allow-all script.ts
# GOOD
deno run --allow-read=./data --allow-net=api.example.com script.ts
--allow-read[=<path>] # File system read
--allow-write[=<path>] # File system write
--allow-net[=<domain>] # Network access
--allow-env[=<var>] # Environment variables
--allow-run[=<program>] # Subprocess execution
/**
* Fetches data from API and caches locally.
*
* Required permissions:
* - --allow-net=api.example.com
* - --allow-read=./cache
* - --allow-write=./cache
*/
export async function fetchData(): Promise<Data> {
// ...
}
// BAD - Hardcoded
const apiKey = "sk-1234";
// GOOD - From environment
const apiKey = Deno.env.get("API_KEY");
if (!apiKey) {
throw new Error("API_KEY required");
}
Run with: deno run --allow-env=API_KEY script.ts
// BAD - Direct JSR/npm imports in source
import { z } from "npm:zod@^3.23.8";
// GOOD - Use import map
import { z } from "zod";
// BAD - Floating versions
"zod": "npm:zod"
// GOOD - Pinned versions
"zod": "npm:zod@^3.23.8"
// BAD - Node.js APIs
const fs = require("fs");
import * as fs from "node:fs";
// GOOD - Deno APIs
await Deno.readTextFile("file.txt");
// BAD - Unnecessary delay
await new Promise(r => setTimeout(r, 100));
// GOOD - Deterministic time
import { FakeTime } from "@std/testing/time";
using time = new FakeTime();
time.tick(100);
# BAD - Overly broad
deno run --allow-all script.ts
# GOOD - Specific
deno run --allow-read=./data script.ts
// BAD - Unnecessary async
async function validate(input: string): Promise<boolean> {
return input.length > 0;
}
// GOOD - Remove async if no await
function validate(input: string): boolean {
return input.length > 0;
}
# Run with watch
deno run --watch src/main.ts
# Type-check
deno check src/**/*.ts
# Format
deno fmt
# Lint
deno lint
# Run all tests
deno test
# With coverage
deno test --coverage=coverage
deno coverage coverage --html
# Watch mode
deno test --watch --fail-fast
# Update dependencies
deno cache --reload
# Update lockfile
deno cache --lock=deno.lock --lock-write
# Run tasks from deno.json
deno task dev
deno task test
deno task coverage
deno.json, never direct imports