TypeScript Programming Guide
Toolchain
- Use Bun as the only package manager and runtime.
- Use the following defaults when scripts are missing:
- Install deps:
bun install
- Type check:
bun run tsc --noEmit
- Lint + fix:
bunx biome check --write <FILE_PATH>
- Format only:
bunx biome format --write <FILE_PATH>
- Tests:
bun test
- Run both Biome and type checks before finishing.
- Avoid introducing ESLint + Prettier in repositories that already use Biome.
- NEVER use
npm, pnpm, yarn, npx, or pnpx.
References
Read these files before deep modifications:
references/biome.md - before editing Biome config, lint rules, or formatting behavior
references/typescript-conventions.md - before changing API types, async flows, or error handling
Missing vs Empty Policy
undefined means missing; null means empty.
- Do not manually create
undefined (= undefined, return undefined, { key: undefined }).
- Represent missing by omitting keys.
Code Standards
You MUST follow all rules and anti-patterns in the example below before writing code.
import type { IncomingHttpHeaders } from "node:http";
import { randomUUID } from "node:crypto";
// RULE: Prefer `type` for unions/intersections and function signatures.
type UserId = string & { readonly __brand: "UserId" };
interface UserRecord {
id: UserId;
email: string;
headers: IncomingHttpHeaders;
createdAt: Date;
}
// RULE: Model fallible operations with discriminated unions.
type LoadUserResult = { ok: true; user: UserRecord } | { ok: false; reason: "not_found" | "timeout" };
async function loadUser(id: UserId): Promise<LoadUserResult> {
if (id.length === 0) {
return { ok: false, reason: "not_found" };
}
return {
ok: true,
user: {
id,
email: "demo@example.com",
headers: {},
createdAt: new Date(),
},
};
}
// RULE: Accept external input as `unknown`, then narrow.
function parsePort(value: unknown): number {
if (typeof value !== "string") {
return 3000;
}
const port = Number(value);
if (!Number.isInteger(port) || port <= 0) {
return 3000;
}
return port;
}
// RULE: Use `satisfies` to validate object shape without widening.
const DEFAULT_CONFIG = {
timeoutMs: 5_000,
retry: 2,
} satisfies {
timeoutMs: number;
retry: number;
};
function formatResult(result: LoadUserResult): string {
// RULE: Use exhaustive checks for discriminated unions.
switch (result.ok) {
case true:
return `ok:${result.user.email}`;
case false:
return `error:${result.reason}`;
default: {
const unreachable: never = result;
return unreachable;
}
}
}
// RULE: Keep indentation shallow with guard clauses.
async function handle(rawId: unknown): Promise<string> {
if (typeof rawId !== "string" || rawId.length === 0) {
return "invalid_user_id";
}
const result = await loadUser(rawId as UserId);
return formatResult(result);
}
// RULE: Throw typed errors with actionable context.
class ServiceError extends Error {
constructor(
message: string,
public readonly code: "timeout" | "internal",
) {
super(message);
this.name = "ServiceError";
}
}
// ANTI-PATTERN: Use `any` as a default escape hatch.
// function parseBad(x: any): any { ... }
// ANTI-PATTERN: Return tuples for complex multi-field results.
// function loadBad(): Promise<[UserRecord | null, string | null]> { ... }
// ANTI-PATTERN: Throw plain strings.
// throw "something failed";
// ANTI-PATTERN: Mix unrelated behavior with boolean control flags.
// function buildReport(data: Item[], debug: boolean, skipCache: boolean) { ... }
// ANTI-PATTERN: Manually manufacture undefined.
// const badPatch = { nickname: undefined };
Delivery Checklist
Before finishing:
- Ensure Biome checks pass on touched files.
- Ensure
tsc --noEmit passes.
- Ensure commands were executed with
bun / bunx only.
- Ensure missing/empty semantics are correct (
undefined=missing, null=empty).
- Ensure exported APIs have explicit, stable types.
- Ensure async code handles failure paths (timeout, cancellation, transport errors).
- Ensure tests are added or updated for behavior changes.
1---2name: typescript-project3description: Comprehensive guide for TypeScript and JavaScript repositories with a Biome-first toolchain. This skill MUST be consulted before writing, reviewing, or refactoring code in TS/JS projects to enforce consistent linting, formatting, type safety, and delivery checks. Use when working on .ts/.tsx/.mts/.cts/.js/.jsx files, tsconfig, package scripts, or tooling in Node.js and frontend projects.4---56# TypeScript Programming Guide78## Toolchain910- Use Bun as the only package manager and runtime.11- Use the following defaults when scripts are missing:12 - Install deps: `bun install`13 - Type check: `bun run tsc --noEmit`14 - Lint + fix: `bunx biome check --write <FILE_PATH>`15 - Format only: `bunx biome format --write <FILE_PATH>`16 - Tests: `bun test`17- Run both Biome and type checks before finishing.18- Avoid introducing ESLint + Prettier in repositories that already use Biome.19- NEVER use `npm`, `pnpm`, `yarn`, `npx`, or `pnpx`.2021## References2223Read these files before deep modifications:2425- `references/biome.md` - before editing Biome config, lint rules, or formatting behavior26- `references/typescript-conventions.md` - before changing API types, async flows, or error handling2728## Missing vs Empty Policy2930- `undefined` means missing; `null` means empty.31- Do not manually create `undefined` (`= undefined`, `return undefined`, `{ key: undefined }`).32- Represent missing by omitting keys.3334## Code Standards3536You MUST follow all rules and anti-patterns in the example below before writing code.3738```ts39import type { IncomingHttpHeaders } from "node:http";40import { randomUUID } from "node:crypto";4142// RULE: Prefer `type` for unions/intersections and function signatures.43type UserId = string & { readonly __brand: "UserId" };4445interface UserRecord {46 id: UserId;47 email: string;48 headers: IncomingHttpHeaders;49 createdAt: Date;50}5152// RULE: Model fallible operations with discriminated unions.53type LoadUserResult = { ok: true; user: UserRecord } | { ok: false; reason: "not_found" | "timeout" };5455async function loadUser(id: UserId): Promise<LoadUserResult> {56 if (id.length === 0) {57 return { ok: false, reason: "not_found" };58 }5960 return {61 ok: true,62 user: {63 id,64 email: "demo@example.com",65 headers: {},66 createdAt: new Date(),67 },68 };69}7071// RULE: Accept external input as `unknown`, then narrow.72function parsePort(value: unknown): number {73 if (typeof value !== "string") {74 return 3000;75 }7677 const port = Number(value);78 if (!Number.isInteger(port) || port <= 0) {79 return 3000;80 }8182 return port;83}8485// RULE: Use `satisfies` to validate object shape without widening.86const DEFAULT_CONFIG = {87 timeoutMs: 5_000,88 retry: 2,89} satisfies {90 timeoutMs: number;91 retry: number;92};9394function formatResult(result: LoadUserResult): string {95 // RULE: Use exhaustive checks for discriminated unions.96 switch (result.ok) {97 case true:98 return `ok:${result.user.email}`;99 case false:100 return `error:${result.reason}`;101 default: {102 const unreachable: never = result;103 return unreachable;104 }105 }106}107108// RULE: Keep indentation shallow with guard clauses.109async function handle(rawId: unknown): Promise<string> {110 if (typeof rawId !== "string" || rawId.length === 0) {111 return "invalid_user_id";112 }113114 const result = await loadUser(rawId as UserId);115 return formatResult(result);116}117118// RULE: Throw typed errors with actionable context.119class ServiceError extends Error {120 constructor(121 message: string,122 public readonly code: "timeout" | "internal",123 ) {124 super(message);125 this.name = "ServiceError";126 }127}128129// ANTI-PATTERN: Use `any` as a default escape hatch.130// function parseBad(x: any): any { ... }131132// ANTI-PATTERN: Return tuples for complex multi-field results.133// function loadBad(): Promise<[UserRecord | null, string | null]> { ... }134135// ANTI-PATTERN: Throw plain strings.136// throw "something failed";137138// ANTI-PATTERN: Mix unrelated behavior with boolean control flags.139// function buildReport(data: Item[], debug: boolean, skipCache: boolean) { ... }140141// ANTI-PATTERN: Manually manufacture undefined.142// const badPatch = { nickname: undefined };143```144145## Delivery Checklist146147Before finishing:148149- Ensure Biome checks pass on touched files.150- Ensure `tsc --noEmit` passes.151- Ensure commands were executed with `bun` / `bunx` only.152- Ensure missing/empty semantics are correct (`undefined`=missing, `null`=empty).153- Ensure exported APIs have explicit, stable types.154- Ensure async code handles failure paths (timeout, cancellation, transport errors).155- Ensure tests are added or updated for behavior changes.