Using any is the fast path to eliminating everything TypeScript is supposed to give you. Type-inferred code is self-documenting and catches bugs at refactor time — not in production at 2am.
Claude Code, guided by CLAUDE.md rules, generates advanced type designs from day one. Here's the exact setup and the patterns it produces.
Step 1: Lock Down Claude Code with CLAUDE.md
## TypeScript Strict Rules
- No `any` — use `unknown` + type guards instead
- No `as T` assertions — implement type guard functions
- No `// @ts-ignore` or `// @ts-expect-error`
- External API responses: Zod parse, not type assertion
- State modeled as Discriminated Union
- Template Literal Types for event names and map keys
- Use `satisfies` for type-checked config objects
- tsconfig: `"strict": true, "noUncheckedIndexedAccess": true`
Write it once. Every Claude Code session follows it automatically.
Pattern 1: Discriminated Union for API State
Instead of isLoading: boolean + data: T | null + error: string | null scattered across your component, use a single union type that's impossible to get into an inconsistent state.
type ApiState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string; retryCount: number };
interface User {
id: string;
name: string;
email: string;
}
function renderUserList(state: ApiState<User[]>): string {
switch (state.status) {
case "idle":
return "Waiting for request...";
case "loading":
return "Loading users...";
case "success":
return state.data.map(u => u.name).join(", ");
case "error":
return `Error (retry ${state.retryCount}): ${state.error}`;
// TypeScript enforces exhaustive handling — no default needed
}
}
Add a new variant to ApiState and the compiler will point out every unhandled branch. That's free regression coverage.
Pattern 2: Template Literal Types for Event Systems
Hard-coded string events like "user-created" or "order_updated" cause silent bugs when they're mistyped. Template Literal Types make the compiler catch them.
type Entity = "user" | "order" | "product";
type Action = "created" | "updated" | "deleted";
// Generates all 9 combinations at the type level
type EventName = `${Entity}.${Action}`;
// "user.created" | "user.updated" | "user.deleted" |
// "order.created" | ... | "product.deleted"
interface UserCreatedPayload { userId: string; email: string }
interface OrderUpdatedPayload { orderId: string; status: string }
interface ProductDeletedPayload { productId: string }
type EventPayloads = {
"user.created": UserCreatedPayload;
"order.updated": OrderUpdatedPayload;
"product.deleted": ProductDeletedPayload;
// Add more as needed — compiler enforces completeness
};
class TypedEventEmitter {
emit<K extends keyof EventPayloads>(
event: K,
payload: EventPayloads[K]
): void {
console.log(event, payload);
}
}
const emitter = new TypedEventEmitter();
// Correct — compiles fine
emitter.emit("user.created", { userId: "u1", email: "a@b.com" });
// Wrong event name — compile error
// emitter.emit("usr.created", { ... });
// Wrong payload shape — compile error
// emitter.emit("user.created", { orderId: "o1", status: "paid" });
No runtime event registry, no magic strings, no silent failures.
Pattern 3: satisfies for Type-Checked Config
satisfies lets you validate that an object conforms to a type while preserving the literal types of its values.
type RouteConfig = {
path: string;
title: string;
auth: boolean;
};
// satisfies validates the shape WITHOUT widening types to RouteConfig
const routes = {
home: { path: "/", title: "Home", auth: false },
profile: { path: "/profile", title: "Profile", auth: true },
admin: { path: "/admin", title: "Admin", auth: true },
} satisfies Record<string, RouteConfig>;
// Derived type — literal string union from the actual keys
type RouteName = keyof typeof routes;
// "home" | "profile" | "admin"
// Type-safe navigation helper
function navigate(route: RouteName): void {
const config = routes[route]; // RouteConfig, fully typed
console.log(`Navigating to ${config.path}`);
}
navigate("profile"); // OK
// navigate("settings"); // Error: not a valid route
Pattern 4: unknown + Type Guard for API Responses
Never trust external data. unknown forces you to validate before use; type guard functions centralize the validation logic.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
// Type guard using Zod parse
function isUser(value: unknown): value is User {
return UserSchema.safeParse(value).success;
}
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${encodeURIComponent(id)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw: unknown = await res.json(); // unknown, not any
const parsed = UserSchema.safeParse(raw);
if (!parsed.success) {
throw new Error(`Invalid user shape: ${parsed.error.message}`);
}
return parsed.data; // User — fully validated
}
If the API changes its response shape, you get a runtime error with a clear message instead of silently corrupting downstream state.
Pattern 5: Utility Type Composition
Claude Code composes standard utility types to generate precise, minimal types from existing ones.
interface Product {
id: string;
name: string;
price: number;
description: string;
createdAt: Date;
updatedAt: Date;
}
// Immutable product card — only display fields, all readonly
type ProductCard = Readonly<Pick<Product, "id" | "name" | "price">>;
// Update form — all fields optional except id, no timestamps
type ProductUpdateInput = Partial<Omit<Product, "id" | "createdAt" | "updatedAt">> & {
id: string;
};
// Extract async return type
async function loadProducts(): Promise<Product[]> {
return fetch("/api/products").then(r => r.json());
}
type LoadedProducts = Awaited<ReturnType<typeof loadProducts>>;
// Product[]
No duplication. When Product changes, all derived types update automatically.
Summary
| Pattern | What It Prevents |
|---|---|
| Discriminated Union (ApiState) | Impossible states like isLoading: true + data: [...]
|
| Template Literal Types | Mistyped event names, missing payload handlers |
unknown + Zod type guard |
Silent API shape drift becoming runtime corruption |
satisfies |
Config typos and invalid route names |
| Utility type composition | Type duplication and manual sync when interfaces change |
Define the constraints in CLAUDE.md. Claude Code applies them to every generation without reminders.
Code Review Pack (¥980) includes /code-review prompt for detecting any usage, unsafe as assertions, and missing type guards across your entire codebase. 👉 https://prompt-works.jp
Top comments (0)