Strict TypeScript Conventions for Agents
Claude CodeCLAUDE.mdCoding
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.
Get this skill
Add the skill file with the CLI, by hand, or over MCP.Save this to your project or home directory so Claude Code can load it.
npx goodprompts add typescript-strict-mode-rulesSaves to
./CLAUDE.mdnpx goodprompts add typescript-strict-mode-rulesSaves to
./CLAUDE.mdSkill file
CLAUDE.md2,907 characters# 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.