Using any in TypeScript is a trap. It silences the compiler, hides bugs until runtime, and spreads like a virus — once you allow it in one place, it shows up everywhere.
Claude Code, with the right setup, generates TypeScript that never touches any. Here's the exact workflow using three battle-tested patterns.
Step 1: Lock Down Claude Code with CLAUDE.md Rules
Claude Code reads CLAUDE.md from your project root as persistent context. Define your TypeScript constraints there and every code generation session automatically follows them.
## TypeScript Strict Rules
- `tsconfig.json` must have `"strict": true`
- `any` is banned — use `unknown` and narrow with type guards
- `as` type assertions are banned — implement type guard functions instead
- `// @ts-ignore` and `// @ts-expect-error` are banned
- All functions must have explicit return type annotations
- External API responses must be typed as `unknown`, validated with zod or type guards
Write it once, enforce it forever.
Pattern 1: Discriminated Union + Result Type
The classic try/catch problem:
// This is what we want to avoid
async function fetchUser(id: string) {
try {
const res = await fetch(`/api/users/${id}`);
return await res.json(); // any
} catch (e: any) { // any again
console.error(e.message);
return null;
}
}
Prompt for Claude Code:
Following the CLAUDE.md TypeScript rules, implement a Result<T, E> type for
type-safe error handling:
- ok: true branch holds data: T
- ok: false branch holds error: E
- Include unwrapOrThrow(), mapResult(), flatMapResult() helpers
- Add AsyncResult<T, E> = Promise<Result<T, E>>
- No any, no as
Generated output:
type Result<T, E> =
| { ok: true; data: T }
| { ok: false; error: E };
type AsyncResult<T, E> = Promise<Result<T, E>>;
function ok<T>(data: T): Result<T, never> {
return { ok: true, data };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
function unwrapOrThrow<T, E>(result: Result<T, E>): T {
if (result.ok) return result.data;
throw result.error;
}
// Error cases as a discriminated union — exhaustive by construction
type ApiError =
| { type: "NOT_FOUND"; id: string }
| { type: "UNAUTHORIZED" }
| { type: "NETWORK_ERROR"; message: string };
async function fetchUser(id: string): AsyncResult<User, ApiError> {
try {
const res = await fetch(`/api/users/${encodeURIComponent(id)}`);
if (res.status === 404) return err({ type: "NOT_FOUND", id });
if (res.status === 401) return err({ type: "UNAUTHORIZED" });
if (!res.ok) return err({ type: "NETWORK_ERROR", message: res.statusText });
const raw: unknown = await res.json();
return ok(parseUser(raw)); // validated, not cast
} catch (e) {
const message = e instanceof Error ? e.message : "unknown";
return err({ type: "NETWORK_ERROR", message });
}
}
Add a new error case to ApiError and the compiler will point out every unhandled branch in your switch statements. That's what exhaustive checking looks like.
Pattern 2: Branded Types to Prevent ID Mix-ups
// Without branded types, this compiles and silently corrupts data
function getOrder(userId: string, orderId: string) { ... }
getOrder(orderId, userId); // swapped — no error
Prompt for Claude Code:
Following the CLAUDE.md TypeScript rules, implement Branded Types for:
UserId, OrderId, ProductId, Email
Requirements:
- Use Brand<T, B> utility type with unique symbol
- Constructor functions named parse* that return Result<BrandedType, ValidationError>
- No any, no as outside the brand-assignment boundary
Generated output:
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
type ValidationError = { field: string; message: string };
function parseUserId(raw: string): Result<UserId, ValidationError> {
if (!raw || raw.trim().length === 0) {
return err({ field: "userId", message: "must be non-empty" });
}
return ok(raw as UserId); // only brand-assignment uses `as`
}
function parseEmail(raw: string): Result<Email, ValidationError> {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!re.test(raw)) {
return err({ field: "email", message: "invalid format" });
}
return ok(raw as Email);
}
// Now the compiler enforces argument order
function processOrder(userId: UserId, orderId: OrderId): void {
console.log(`Processing order ${orderId} for user ${userId}`);
}
// processOrder(orderId, userId); → TypeScript error: Argument of type 'OrderId'
// is not assignable to parameter of type 'UserId'
The bug that was invisible at runtime is now a compile-time error.
Pattern 3: Mapped Types for API Response Transformation
Manually converting snake_case API responses to camelCase — and keeping types in sync — is tedious and error-prone.
Prompt for Claude Code:
Following the CLAUDE.md TypeScript rules, implement these type utilities:
1. SnakeToCamelCase<S extends string> — type-level string transformation
2. SnakeKeysToCamelCase<T> — recursively transform all object keys
3. DeepReadonly<T>
4. DeepPartial<T>
No any, no as — pure type-level operations only
Generated output:
type SnakeToCamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<SnakeToCamelCase<Tail>>}`
: S;
type SnakeKeysToCamelCase<T> = T extends object
? {
[K in keyof T as SnakeToCamelCase<K & string>]: T[K] extends object
? SnakeKeysToCamelCase<T[K]>
: T[K];
}
: T;
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// Usage — the API type drives the frontend type
type ApiUser = {
user_id: string;
created_at: string;
profile_image_url: string | null;
};
type FrontendUser = SnakeKeysToCamelCase<ApiUser>;
// → { userId: string; createdAt: string; profileImageUrl: string | null }
// Make it immutable for safe passing through the component tree
type ReadonlyUser = DeepReadonly<FrontendUser>;
When the API schema changes, the derived frontend types update automatically. No manual sync required.
Putting It Together
Here's a realistic end-to-end flow:
// 1. Parse and brand external IDs at the boundary
const userIdResult = parseUserId(req.params.id);
if (!userIdResult.ok) {
return res.status(400).json(userIdResult.error);
}
const userId = userIdResult.data; // UserId (branded)
// 2. Fetch with type-safe error handling
const fetchResult = await fetchUser(userId);
if (!fetchResult.ok) {
switch (fetchResult.error.type) {
case "NOT_FOUND": return res.status(404).json({ id: fetchResult.error.id });
case "UNAUTHORIZED": return res.status(401).json({ error: "unauthorized" });
case "NETWORK_ERROR": return res.status(503).json({ error: fetchResult.error.message });
}
}
// 3. Transform to frontend shape
const user: FrontendUser = transformKeys(fetchResult.data);
Every error case is handled. Every ID is the right type. No any, no as, no runtime surprises.
Summary
| Pattern | Problem Solved |
|---|---|
| Result type + Discriminated Union | Type-safe error handling without any in catch |
| Branded Types | Prevent ID mix-ups caught at compile time |
| Mapped Types | Auto-derive frontend types from API schema |
Define the rules in CLAUDE.md once. Claude Code applies them consistently to every generation.
Code Review Pack (¥980) includes /code-review for detecting any usage and unsafe type patterns. 👉 https://prompt-works.jp
Top comments (0)