TypeScript is where Claude Code genuinely earns its place in a development workflow. Types are verbose to write correctly — inference helps, but generics, conditional types, and complex mapped types require real understanding of the type system. Claude Code handles TypeScript’s more advanced features well and rarely falls back to any.
This guide covers using Claude Code for TypeScript-specific work: writing proper generics, using utility types correctly, designing discriminated unions, and tightening loose types across a codebase.
Why TypeScript + Claude Code Works
The key reason Claude Code handles TypeScript better than most AI tools: it reasons about the semantic meaning of types, not just the syntax. When you ask for “a type-safe API client,” Claude doesn’t just add type annotations to an untyped function — it designs the generic structure that makes the whole thing type-safe end-to-end.
It also understands what you’re trying to prevent. When you say “I want TypeScript to catch it if someone passes the wrong ID type,” Claude writes a branded/nominal type rather than just using string.
Setting Up Claude Code for TypeScript Projects
Your CLAUDE.md should include TypeScript-specific conventions — see the CLAUDE.md guide for the full setup. Key TypeScript conventions to document:
## TypeScript Rules
- No 'any' — use 'unknown' if necessary, then narrow
- Prefer type over interface for unions/intersections
- Prefer interface for object shapes that will be extended
- Use strict mode — tsconfig.json has strict:true
- Utility types: use Record<K,V> not {[key: K]: V}
- Avoid as casts — if you need to cast, explain why in a comment
Generics: The Most Common Claude Code Use Case
Generics are where TypeScript gets powerful and verbose. Claude Code writes them correctly and concisely.
API Response Types
The classic generic wrapper:
// What Claude generates when you ask for a typed API client
type ApiResponse<T> =
| { success: true; data: T; error: null }
| { success: false; data: null; error: ApiError };
async function fetchApi<T>(endpoint: string): Promise<ApiResponse<T>> {
try {
const res = await fetch(endpoint);
if (!res.ok) {
return { success: false, data: null, error: await parseError(res) };
}
const data = await res.json() as T;
return { success: true, data, error: null };
} catch (e) {
return { success: false, data: null, error: toApiError(e) };
}
}
Usage is fully type-safe:
const result = await fetchApi<User>('/api/user/123');
if (result.success) {
console.log(result.data.name); // TypeScript knows data is User
} else {
console.log(result.error.message); // TypeScript knows error exists
}
Ask Claude to generate this pattern for your specific API structure — it adapts to your error shapes, authentication headers, and base URL configuration.
Generic Repository Pattern
Write a generic Repository<T> class that wraps Prisma.
It should have: findById(id), findMany(where), create(data),
update(id, data), delete(id).
T is the model type, use Prisma's types for where/create/update.
Claude generates:
class Repository<T extends { id: string }> {
constructor(private model: PrismaClient[ModelName]) {}
async findById(id: string): Promise<T | null> { ... }
async findMany(where?: Prisma.WhereInput<T>): Promise<T[]> { ... }
async create(data: Prisma.CreateInput<T>): Promise<T> { ... }
async update(id: string, data: Prisma.UpdateInput<T>): Promise<T> { ... }
async delete(id: string): Promise<void> { ... }
}
This is the kind of boilerplate that’s tedious to write by hand but perfectly consistent when Claude generates it.
Discriminated Unions for State Machines
Discriminated unions are the most underused TypeScript feature. They eliminate entire categories of “undefined is not an object” runtime errors.
I have a component that can be in 4 states: loading, error, empty, populated.
Currently I'm using separate boolean flags (isLoading, hasError, isEmpty).
Convert this to a discriminated union type and update the component.
[paste component]
Claude generates:
type ComponentState =
| { status: 'loading' }
| { status: 'error'; error: string }
| { status: 'empty' }
| { status: 'populated'; data: Item[] };
Now TypeScript enforces that you handle all four cases. In the populated case, TypeScript narrows state.data to Item[] — no optional chaining needed.
If you’re into this pattern, the Claude Skills 360 bundle includes a state-machine-types skill that scaffolds these union types for common patterns (fetch states, form states, wizard steps).
Branded Types for Domain Safety
Passing the wrong string ID to the wrong function is a classic TypeScript footgun — userId: string and orderId: string are structurally identical, so TypeScript doesn’t catch when you pass one as the other.
Add branded types to prevent mix-ups between UserId, OrderId, and ProductId.
These are all strings at runtime but should be distinct at compile time.
Claude generates:
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type ProductId = Brand<string, 'ProductId'>;
// Constructor functions cast once
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: string): OrderId => id as OrderId;
Now passing a UserId to a function expecting OrderId is a compile error. The as casts happen once at the boundary (API response, database query) and nowhere else.
Utility Types: Using Them Correctly
TypeScript’s built-in utility types (Partial, Required, Pick, Omit, Record) are well known but often used wrong. Claude Code applies them correctly.
Partial Updates
I have a User type with 10 fields.
I need a function to update any subset of them.
Don't use Partial<User> directly — it would let you pass an empty object.
I want at least one field to be required.
Claude generates:
type RequireAtLeastOne<T> = {
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
}[keyof T];
function updateUser(id: string, update: RequireAtLeastOne<User>) { ... }
This is the kind of utility type composition that takes real TypeScript knowledge — Claude writes it correctly and explains what each step does.
Conditional Types
Conditional types are powerful but syntax-heavy:
I want a type that extracts the resolved value from a Promise type.
If I pass Promise<User> I get User back. If I pass User I get User back.
Works recursively for nested promises.
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
(TypeScript 4.5+ has Awaited built-in, but Claude will tell you this rather than generating redundant code.)
Template Literal Types
One of TypeScript’s more exotic features — useful for typed string patterns:
I have an API with routes like GET /users/:id, POST /users, DELETE /users/:id.
I want TypeScript to validate route strings at compile time.
Create a type that accepts only valid route patterns from our API.
Claude generates:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiRoute =
| 'GET /users'
| 'GET /users/:id'
| 'POST /users'
| 'PUT /users/:id'
| 'DELETE /users/:id'
| 'GET /orders'
// ... etc
type TypedFetch = {
(route: ApiRoute, options?: RequestInit): Promise<Response>;
};
Or for parameterized routes:
type PathParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | PathParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
// PathParams<'/users/:id/orders/:orderId'> = 'id' | 'orderId'
Explaining what you need precisely gets you working template literal types. If the explanation is vague, ask Claude to explain what it generated before using it.
Improving Type Coverage
If you have an existing TypeScript codebase with loose types, Claude Code systematically tightens them.
Audit and Fix any
Find every use of 'any' in src/.
For each, read the surrounding code and classify:
- Inferred: the type can be derived from how it's used
- Shape unknown: the data shape needs to be documented
- Truly dynamic: runtime dispatch that genuinely needs unknown + narrowing
Report as a table with file, line, classification, and suggested fix.
Don't change anything yet.
After reviewing:
Fix all 'inferred' cases. Leave 'shape unknown' and 'truly dynamic' for me to review.
This targeted approach fixes ~70% of any usages without touching the ones that need thought.
Strengthening Function Signatures
The function processResponse(data: any, config: any): any
is called in 12 places.
Read all the call sites and infer what types are actually being passed.
Suggest a proper signature — or multiple overloads if different callers pass different types.
Claude reads the actual call sites and infers concrete types, rather than just changing any to unknown everywhere.
Using Zod with Claude Code
If your project validates external data (API responses, form inputs, environment variables), Zod + TypeScript is the standard approach. Claude Code generates Zod schemas that derive TypeScript types:
Create a Zod schema for a User object from our API.
Fields: id (UUID string), email (valid email),
name (string, 2-100 chars), role ('admin' | 'member' | 'viewer'),
createdAt (ISO date string → Date object),
avatar (optional URL string).
Also export the inferred TypeScript type.
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['admin', 'member', 'viewer']),
createdAt: z.string().datetime().transform(s => new Date(s)),
avatar: z.string().url().optional(),
});
export type User = z.infer<typeof UserSchema>;
The z.infer trick gives you a TypeScript type from the Zod schema for free — no duplication. If you’re using Zod extensively, the Claude Skills 360 bundle includes Zod schema generation and validation skills that follow the pattern above.
tsconfig Strictness
If you’re starting a new project or tightening an existing one:
My current tsconfig has strict: false and some code uses loose patterns.
Show me the recommended tsconfig settings for a production TypeScript app.
Then analyze what errors enabling strict would create in my current codebase —
I'll fix those before enabling it.
Claude outputs the strict tsconfig options with explanations, then identifies what current code would fail. This makes enabling strict mode a planned migration rather than a surprise.
TypeScript and Testing
TypeScript types catch one class of bugs. Tests catch a different class. Both are necessary — see the testing and debugging guide for how to write TypeScript-aware tests. The key point: type errors don’t replace tests, and tests don’t replace types.
For the full TypeScript development workflow with skills for common patterns, Claude Skills 360 includes TypeScript utilities, generic patterns, and schema generation tasks. Start with the free tier — 360 skills including TypeScript patterns, no payment required.