Advanced TypeScript’s type system can encode complex runtime behavior as compile-time guarantees — routing constraints, API response shapes, state machine transitions. Claude Code writes deep TypeScript types that are genuinely useful rather than just syntactically complex.
This guide covers advanced TypeScript with Claude Code: mapped types, conditional types, template literal types, the infer keyword, and practical patterns that prevent entire categories of runtime errors.
Mapped Types
Create a mapped type that makes all properties of a type
optional at any depth (deep partial). The built-in Partial<T>
only goes one level deep.
// Recursive conditional type — goes as deep as needed
type DeepPartial<T> = T extends object
? T extends (infer U)[]
? DeepPartial<U>[] // Handle arrays
: T extends Map<infer K, infer V>
? Map<K, DeepPartial<V>> // Handle Map
: { [P in keyof T]?: DeepPartial<T[P]> } // Handle objects
: T;
// Usage
interface Config {
database: { host: string; port: number; ssl: { enabled: boolean; cert: string } };
redis: { url: string; maxConnections: number };
}
type PartialConfig = DeepPartial<Config>;
// Now valid:
const override: PartialConfig = {
database: { ssl: { enabled: true } } // Missing host, port, cert — all optional
};
// Mapped type transformations — practical utilities
// Remove readonly from all properties
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
// Add readonly to all properties (immutable by default)
type Immutable<T> = { readonly [P in keyof T]: Immutable<T[P]> };
// Type that requires at least one property from T
type AtLeastOne<T, Keys extends keyof T = keyof T> =
Keys extends keyof T ? Pick<T, Keys> & Partial<Omit<T, Keys>> : never;
// Filter T to only properties with values matching ValueType
type FilterProperties<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K]
};
// Pick only functions from an object type
type MethodsOnly<T> = FilterProperties<T, (...args: any[]) => any>;
interface UserService {
userId: string;
getUser: (id: string) => Promise<User>;
updateUser: (id: string, data: Partial<User>) => Promise<User>;
deleteUser: (id: string) => Promise<void>;
}
type UserServiceMethods = MethodsOnly<UserService>;
// Result: { getUser: ...; updateUser: ...; deleteUser: ... }
// userId is excluded (not a function)
Template Literal Types
Create types that enforce URL path patterns at compile time.
Routes like '/users/:userId/orders/:orderId' should extract
parameter names automatically.
// Extract parameter names from route string
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
type RouteParams<Path extends string> = {
[K in ExtractParams<Path>]: string;
};
// Now route params are type-safe:
type OrderParams = RouteParams<'/users/:userId/orders/:orderId'>;
// Result: { userId: string; orderId: string }
function buildRoute<P extends string>(
template: P,
params: RouteParams<P>,
): string {
return Object.entries(params).reduce(
(path, [key, value]) => path.replace(`:${key}`, value as string),
template,
);
}
// Type-checked — TypeScript knows what params are required:
buildRoute('/users/:userId/orders/:orderId', { userId: '123', orderId: '456' }); // ✅
buildRoute('/users/:userId/orders/:orderId', { userId: '123' }); // ❌ Missing orderId
// Event system with type-safe event names
type EventMap = {
'user:created': { userId: string; email: string };
'user:deleted': { userId: string };
'order:placed': { orderId: string; total: number };
'order:cancelled': { orderId: string; reason: string };
};
// Extract all event names for a given prefix
type EventsWithPrefix<
Events extends Record<string, any>,
Prefix extends string
> = {
[K in keyof Events as K extends `${Prefix}:${string}` ? K : never]: Events[K];
};
type UserEvents = EventsWithPrefix<EventMap, 'user'>;
// Result: { 'user:created': {...}; 'user:deleted': {...} }
class TypedEventEmitter<Events extends Record<string, unknown>> {
private listeners = new Map<string, Set<Function>>();
on<E extends keyof Events>(event: E, callback: (data: Events[E]) => void): void {
if (!this.listeners.has(event as string)) {
this.listeners.set(event as string, new Set());
}
this.listeners.get(event as string)!.add(callback);
}
emit<E extends keyof Events>(event: E, data: Events[E]): void {
this.listeners.get(event as string)?.forEach(cb => cb(data));
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('user:created', ({ userId, email }) => { /* userId and email are typed */ });
emitter.emit('order:placed', { orderId: '123', total: 99.99 });
Conditional Types and Infer
Create a type that unwraps Promise, Array, and Result types
to get the inner value type.
// Unwrap any single-level container
type Unwrap<T> =
T extends Promise<infer R> ? R :
T extends Array<infer R> ? R :
T extends Set<infer R> ? R :
T extends Map<any, infer R> ? R :
T extends { value: infer R } ? R : // Handles Result<T> pattern
T;
// Deep unwrap (Promise<Array<Promise<string>>> → string)
type DeepUnwrap<T> =
T extends Promise<infer R> | Array<infer R> | Set<infer R>
? DeepUnwrap<R>
: T;
// Extract return type recursively (follow async)
type AsyncReturnType<T extends (...args: any[]) => any> =
Awaited<ReturnType<T>>;
// Infer function parameter types
type FirstParam<T extends (...args: any[]) => any> =
T extends (first: infer P, ...rest: any[]) => any ? P : never;
// Build a type-safe builder pattern
type BuilderSetters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => Builder<T>;
};
class Builder<T extends object> {
private data: Partial<T> = {};
// Dynamically generated set* methods
set<K extends keyof T>(key: K, value: T[K]): this {
this.data[key] = value;
return this;
}
build(): T {
return this.data as T;
}
}
Discriminated Union Exhaustiveness
// Order state machine — exhaustive type-checking
type OrderState =
| { status: 'pending'; placedAt: Date }
| { status: 'confirmed'; confirmedAt: Date; estimatedDelivery: Date }
| { status: 'shipped'; trackingNumber: string; carrier: string }
| { status: 'delivered'; deliveredAt: Date }
| { status: 'cancelled'; reason: string; cancelledAt: Date };
function getOrderMessage(order: OrderState): string {
switch (order.status) {
case 'pending':
return `Order placed at ${order.placedAt.toLocaleDateString()}`;
case 'confirmed':
return `Estimated delivery: ${order.estimatedDelivery.toLocaleDateString()}`;
case 'shipped':
return `Tracking: ${order.trackingNumber} via ${order.carrier}`;
case 'delivered':
return `Delivered on ${order.deliveredAt.toLocaleDateString()}`;
case 'cancelled':
return `Cancelled: ${order.reason}`;
default:
// TypeScript guarantees this is unreachable if all cases are handled
const _exhaustive: never = order;
throw new Error(`Unknown status: ${JSON.stringify(_exhaustive)}`);
}
}
TypeScript Compiler API for Code Generation
Use the TypeScript compiler API to generate API client SDK
from TypeScript interface definitions.
// scripts/generate-sdk.ts
import ts from 'typescript';
import * as fs from 'fs';
function generateClientFromInterfaces(sourceFile: string): string {
const program = ts.createProgram([sourceFile], {});
const checker = program.getTypeChecker();
const source = program.getSourceFile(sourceFile)!;
const methods: string[] = [];
// Visit all nodes looking for interface declarations
ts.forEachChild(source, node => {
if (!ts.isInterfaceDeclaration(node)) return;
const interfaceName = node.name.text;
if (!interfaceName.endsWith('Service')) return;
node.members.forEach(member => {
if (!ts.isMethodSignature(member) || !member.name) return;
const methodName = (member.name as ts.Identifier).text;
const type = checker.getTypeAtLocation(member);
const callSig = type.getCallSignatures()[0];
if (!callSig) return;
const params = callSig.parameters.map(p =>
`${p.name}: ${checker.typeToString(checker.getTypeOfSymbolAtLocation(p, member))}`
).join(', ');
const returnType = checker.typeToString(callSig.getReturnType());
// Generate HTTP client method
methods.push(`
async ${methodName}(${params}): ${returnType} {
return this.request('${methodName}', { ${callSig.parameters.map(p => p.name).join(', ')} });
}`);
});
});
return `// AUTO-GENERATED — do not edit manually
export class GeneratedClient {
constructor(private baseUrl: string, private token: string) {}
private async request(method: string, params: any) {
const res = await fetch(\`\${this.baseUrl}/api/\${method}\`, {
method: 'POST',
headers: { 'Authorization': \`Bearer \${this.token}\`, 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!res.ok) throw new Error(\`\${method} failed: \${res.statusText}\`);
return res.json();
}
${methods.join('\n')}
}`;
}
// Run: ts-node scripts/generate-sdk.ts src/services/*.ts
const output = generateClientFromInterfaces(process.argv[2]);
fs.writeFileSync('src/generated/client.ts', output);
For the branded types and discriminated union patterns that complement these utilities, see the type safety guide. For TypeScript with Next.js App Router including server component types, see the Next.js App Router guide. The Claude Skills 360 bundle includes TypeScript skill sets covering advanced type utilities, codegen patterns, and type-driven development. Start with the free tier to try advanced TypeScript code generation.