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.
The skill file
Claude Code · CLAUDE.mdCLAUDE.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 repoSave 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 skillLoading forks…