TypeScript CLAUDE.md Claude Code AI coding rules type safety configuration

TypeScript CLAUDE.md Rules: A Production-Ready Template

The Prompt Shelf ·

Generic CLAUDE.md files produce generic TypeScript. The rules that work well for a Python backend do not transfer directly — TypeScript has its own failure modes, its own anti-patterns, and its own set of things you have to keep reminding AI coding assistants about.

This is a production-tuned CLAUDE.md template for TypeScript projects. Take the sections that apply to your stack and drop the rest.

The Core Type Safety Block

The most frequent TypeScript issues with AI-generated code: any everywhere, missing null checks, type assertions instead of type narrowing. These rules address all three:

## Type Safety

- **Never use `any`** unless you are bridging a third-party library with no types. If you must use `any`, add an inline comment explaining why.
- Use `unknown` for values of unknown shape. Narrow with type guards before use.
- Prefer `satisfies` over `as` for type assertions. `as` suppresses errors; `satisfies` validates them.
- Do not use non-null assertion (`!`) on user-provided data or API responses. Check for null explicitly.
- Use strict mode settings: `strict: true`, `noUncheckedIndexedAccess: true`, `exactOptionalPropertyTypes: true`.
- When narrowing a union, handle every case. Do not use `else` as a catch-all that silently drops unknown variants.

Why satisfies vs as matters

type Config = { port: number; host: string };

// BAD: as suppresses type errors
const config = { port: "3000", host: "localhost" } as Config; // no error, wrong type

// GOOD: satisfies catches errors at declaration
const config = { port: 3000, host: "localhost" } satisfies Config; // correct

Add this distinction to your CLAUDE.md if your team has been using as as a habit.

Import and Module Rules

## Imports

- Use path aliases defined in `tsconfig.json` (`@/components`, `@/lib`, etc.) — not relative paths that traverse more than one directory level.
- Import types with `import type` when the import is only used for type annotations. This prevents runtime issues with circular references and reduces bundle size.
- Do not barrel-import everything from an index file if you only need one export. Import directly from the source file.
- Keep imports grouped: stdlib → third-party → internal. One blank line between groups.
- Do not import from `../internal` or `../private` directories from outside the module's boundary.

Example of the import type rule:

// BAD: imports the runtime value when only the type is needed
import { User } from "./user";

// GOOD: zero runtime cost
import type { User } from "./user";

// Runtime import when the value itself is needed
import { createUser } from "./user";

Error Handling Patterns

AI-generated TypeScript often uses bare try/catch with any error types, or swallows errors silently. These rules enforce the patterns that survive production:

## Error Handling

- Catch blocks must use `unknown` type: `catch (error: unknown)`. Never `catch (e: any)`.
- Narrow caught errors before accessing properties: `if (error instanceof Error) { ... }`
- Do not swallow errors silently. If you catch and do not rethrow, you must log or handle visibly.
- For async functions, prefer explicit error handling over relying on unhandled promise rejection handlers.
- Use a typed Result pattern for operations that are expected to fail (user input, network requests). Do not rely on exceptions for control flow.
- Never throw plain strings. Throw `Error` instances or custom error classes.

A typed Result pattern worth including in your codebase:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await db.users.findById(id);
    if (!user) return { ok: false, error: new Error(`User ${id} not found`) };
    return { ok: true, value: user };
  } catch (error: unknown) {
    return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
  }
}

Async and Promise Rules

## Async/Await

- `await` every async call that can fail. Do not fire-and-forget without explicit error handling.
- Do not mix `.then()` chains with `await` in the same function.
- For concurrent operations, use `Promise.all()` (or `Promise.allSettled()` when partial failure is acceptable) instead of sequential awaits.
- Mark functions as `async` only if they contain `await`. Do not return `Promise.resolve()` from non-async functions unnecessarily.
- Use `AbortController` for cancellable async operations (fetch, long-running timers).

Functional Component Rules (React/Next.js)

If your project uses React:

## React Components

- Functional components only. No class components.
- Co-locate component types: `type Props = { ... }` at the top of the file, not in a separate types file.
- Use `React.FC` only if you need `children` typed. Otherwise, type props directly.
- Event handlers: `handleClick` not `onClick` for the function name. Use `onClick={handleClick}` in JSX.
- No inline function definitions for callbacks that get passed to `useEffect` dependencies.
- Keys in lists must be stable and unique — not array indices.
- `useEffect` cleanup: return a cleanup function for subscriptions, timers, and event listeners.

Testing Standards

## Testing

- Test files co-located with source: `foo.ts``foo.test.ts`
- Describe blocks named after the module under test: `describe('createUser', ...)`
- Each `it` block tests one behavior and has one assertion group
- Mock at the boundary: mock HTTP calls with MSW, mock filesystem with memfs, mock time with `vi.useFakeTimers()`
- Test the public interface, not implementation details
- Do not use `as` or type assertions in tests to work around type errors — fix the type
- Snapshot tests are allowed only for static UI components; not for API responses or business logic

Banned Patterns

These go in your CLAUDE.md as explicit prohibitions:

## Patterns to Avoid

- `// @ts-ignore` — Fix the type error, do not hide it. Use `// @ts-expect-error` with an explanation if suppression is truly necessary.
- `Object.keys(obj).forEach` when you need the key's type — use typed `for...of` loops instead.
- `JSON.parse()` without validation — use a schema library (zod, valibot) and parse the result.
- `process.env.FOO` without checking for undefined — always provide a fallback or throw on startup.
- Mutating function arguments — return a new object/array.
- `!important` in CSS-in-JS (styled-components, Emotion) — use specificity, not brute force.

The Complete Template

Here is everything assembled into a single drop-in block:

## TypeScript Rules

### Type Safety
- Never use `any`. Use `unknown` and narrow.
- Prefer `satisfies` over `as` for type assertions.
- No non-null assertion `!` on user data or API responses.
- Narrow all union cases explicitly — no catch-all `else`.

### Imports
- Path aliases over deep relative paths.
- `import type` for type-only imports.
- Do not barrel-import when one export is needed.

### Error Handling
- `catch (error: unknown)` — never `catch (e: any)`.
- Narrow caught errors before accessing properties.
- No silent error swallowing.
- No throwing plain strings.

### Async
- Await every fallible async call.
- Use `Promise.all` for concurrent operations.
- Add `AbortController` for cancellable fetches.

### Testing
- Co-locate tests with source.
- Mock at the boundary: MSW for HTTP, memfs for filesystem.
- No `as` in tests to bypass type errors.

### Banned
- `@ts-ignore`
- `JSON.parse()` without schema validation
- `process.env.FOO` without undefined check
- Mutating function arguments

Tuning for Your Project

The template above covers the highest-frequency issues. For your specific project, add:

  • ORM/query patterns: “Use Drizzle’s eq() for WHERE clauses, not string templates”
  • State management: “Server state in TanStack Query, client state in Zustand — nothing in React context”
  • API contracts: “All external API calls through the @/lib/api module — never fetch directly in components”
  • Validation entry points: “Parse and validate all input at the route handler layer using Zod before it reaches business logic”

Keep the total CLAUDE.md under 200 lines. Rules beyond that get diluted — Claude Code has to pick which ones to follow when there are too many. Prioritize the rules that would cause the most damage if violated, not the ones that are merely preferences.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules