Files
AGENTS/rules/languages/typescript.md
m3tm3re 8910413315 feat(rules): add initial rule files for concerns, languages, and frameworks
Concerns (6 files):
- coding-style.md (163 lines): patterns, anti-patterns, error handling, SOLID
- naming.md (105 lines): naming conventions table per language
- documentation.md (149 lines): docstrings, WHY vs WHAT, README standards
- testing.md (134 lines): AAA pattern, mocking philosophy, TDD
- git-workflow.md (118 lines): conventional commits, branch naming, PR format
- project-structure.md (82 lines): directory layout, entry points, config placement

Languages (4 files):
- python.md (224 lines): uv, ruff, pyright, pytest, pydantic, idioms, anti-patterns
- typescript.md (150 lines): strict mode, discriminated unions, satisfies, as const
- nix.md (129 lines): flake structure, module patterns, alejandra, anti-patterns
- shell.md (100 lines): set -euo pipefail, shellcheck, quoting, POSIX

Frameworks (1 file):
- n8n.md (42 lines): workflow design, node patterns, Error Trigger, security

Context budget: 975 lines (concerns + python) < 1500 limit

Refs: T6-T16 of rules-system plan
2026-02-17 19:05:45 +01:00

2.8 KiB

TypeScript Patterns

Strict tsconfig

Always enable strict mode and key safety options:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Discriminated Unions

Use discriminated unions for exhaustive type safety:

type Result =
  | { success: true; data: string }
  | { success: false; error: Error };

function handleResult(result: Result): string {
  if (result.success) {
    return result.data;
  }
  throw result.error;
}

Branded Types

Prevent type confusion with nominal branding:

type UserId = string & { readonly __brand: unique symbol };
type Email = string & { readonly __brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function sendEmail(email: Email, userId: UserId) {}

satisfies Operator

Use satisfies for type-safe object literal inference:

const config = {
  port: 3000,
  host: "localhost",
} satisfies {
  port: number;
  host: string;
  debug?: boolean;
};

config.port; // number
config.host; // string

as const Assertions

Freeze literal types with as const:

const routes = {
  home: "/",
  about: "/about",
  contact: "/contact",
} as const;

type Route = typeof routes[keyof typeof routes];

Modern Features

// Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers<string>();

// Object.groupBy()
const users = [
  { name: "Alice", role: "admin" },
  { name: "Bob", role: "user" },
];
const grouped = Object.groupBy(users, u => u.role);

// using statement for disposables
class Resource implements Disposable {
  async [Symbol.asyncDispose]() {
    await this.cleanup();
  }
}
async function withResource() {
  using r = new Resource();
}

Toolchain

Prefer modern tooling:

  • Runtime: bun or tsx (no tsc for execution)
  • Linting: biome (preferred) or eslint
  • Formatting: biome (built-in) or prettier

Anti-Patterns

Avoid these TypeScript patterns:

// NEVER use as any
const data = response as any;

// NEVER use @ts-ignore
// @ts-ignore
const value = unknownFunction();

// NEVER use ! assertion (non-null)
const element = document.querySelector("#foo")!;

// NEVER use enum (prefer union)
enum Status { Active, Inactive } // ❌

// Prefer const object or union
type Status = "Active" | "Inactive"; // ✅
const Status = { Active: "Active", Inactive: "Inactive" } as const; // ✅

Indexed Access Safety

With noUncheckedIndexedAccess, handle undefined:

const arr: string[] = ["a", "b"];
const item = arr[0]; // string | undefined

const item2 = arr.at(0); // string | undefined

const map = new Map<string, number>();
const value = map.get("key"); // number | undefined