Skip to content
Skill · Claude CodeCLAUDE.mdCoding

Strict TypeScript Conventions for Agents

House rules for strict TypeScript: no any, discriminated unions instead of enums, exhaustive switches, zod at every boundary — written so an agent can self-check each rule.

Use when: Add to any TypeScript codebase with strict mode enabled when you want agents to hold a hard line on type safety instead of reaching for any, casts, or non-null assertions under pressure.

@libraryNewcomer
0 upvotes0 forks0 comments

The skill file

Claude Code · CLAUDE.md
CLAUDE.md2907 chars
# TypeScript Conventions These rules apply to all `.ts`/`.tsx` files. `tsconfig.json` has `strict`, `noUncheckedIndexedAccess`, and `exactOptionalPropertyTypes` enabled — code must typecheck under all three. Verify with `pnpm typecheck` before claiming a task is done. ## Banned constructs - `any` — explicit or implicit. Use `unknown` and narrow it. If a third-party type forces `any`, isolate it behind a typed wrapper function and leave a comment naming the upstream issue. - `as` casts to silence errors. Allowed only for `as const` and widening-safe cases like `as unknown as T` inside test factories — never in `src/` production paths. - Non-null assertions (`x!`). Prove non-nullness with a guard, or restructure so the type system already knows. - `@ts-ignore` and `@ts-expect-error` in production code. In tests, `@ts-expect-error` is fine when the error is the point of the test. - `enum`. See below. ## Discriminated unions over enums Model variants as tagged unions, not enums or boolean flags: ```ts type PaymentState = | { status: "pending" } | { status: "settled"; settledAt: Date; reference: string } | { status: "failed"; reason: string; retryable: boolean }; ``` - The discriminant is always a string literal named `status`, `kind`, or `type` — pick one per domain and stay consistent. - Data that only exists in one state lives only in that variant. If you find yourself writing `settledAt?: Date` on a shared shape, split the union. - Two booleans that can't both be true are a union in disguise — refactor. ## Exhaustive switches Every `switch` over a union ends with an exhaustiveness check: ```ts default: { const _exhaustive: never = state; throw new Error(`Unhandled state: ${JSON.stringify(_exhaustive)}`); } ``` When you add a variant to a union, the compiler must point you at every switch to update. If it doesn't, the switch is missing its `never` check — add one. ## Zod at boundaries - Every piece of external data — HTTP request bodies, query params, env vars, webhook payloads, `JSON.parse` results, rows from untyped clients — is parsed with a zod schema before use. No `as ApiResponse` on fetch results. - Schemas live next to the boundary that uses them; derive the type with `z.infer<typeof schema>` rather than writing the type twice. - Inside the validated core, do not re-validate — trust the types. Validation happens once, at the edge. - Use `safeParse` where failure is expected (user input) and `parse` where failure is a bug (env vars at startup). ## General - Prefer `readonly` arrays/properties on public signatures; mutate only locals. - Return types are explicit on exported functions; inference is fine for internals. - Narrow with type guards (`function isFoo(x): x is Foo`) instead of casting after a runtime check. - New utility types require justification — prefer plain unions and interfaces over clever mapped-type machinery.

Install

drop into your repo

Save this to your project or home directory so Claude Code can load it.

path./CLAUDE.md
Discussion

What people are saying

Forks

0 forks of this skill
Loading forks…