# Typescript Project

> 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.

- Skill: `observerw/typescript-project` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add observerw/typescript-project`
- Raw SKILL.md: https://api.skillmd.com/api/skills/observerw/typescript-project/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: observerw (https://skillmd.com/u/observerw)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/observerw/typescript-project

---


# 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.

```ts
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.

