Advanced TypeScript patterns for type-safe, maintainable code using sophisticated type system features. Use when building type-safe APIs, implementing complex domain models, or leveraging TypeScript's advanced type capabilities.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Expert guidance for leveraging TypeScript's advanced type system features to build robust, type-safe applications with sophisticated type inference, compile-time guarantees, and maintainable domain models.
Type selection based on conditions:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Extract function return types
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => { name: string; age: number };
type Result = ReturnTypeOf<Fn>; // { name: string; age: number }
// Extract array element types
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Items = ElementOf<string[]>; // string
Use cases:
Transform object types systematically:
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Pick specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Create API response type
type UserResponse = Omit<User, 'password'>;
// Create update type (all optional)
type UserUpdate = Partial<User>;
// Create creation type (no id)
type UserCreate = Omit<User, 'id'>;
Advanced mapping:
// Add prefix to all keys
type Prefixed<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${string & K}`]: T[K];
};
type Events = {
click: MouseEvent;
focus: FocusEvent;
};
type Handlers = Prefixed<Events, 'on'>;
// { onclick: MouseEvent; onfocus: FocusEvent }
String type manipulation at compile time:
// Event handler types
type EventNames = 'click' | 'focus' | 'blur';
type EventHandlers = `on${Capitalize<EventNames>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// URL path types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/api/${'users' | 'posts' | 'comments'}`;
type Route = `${HTTPMethod} ${Endpoint}`;
// 'GET /api/users' | 'POST /api/users' | ...
// CSS property types
type CSSUnit = 'px' | 'em' | 'rem' | '%';
type Size = `${number}${CSSUnit}`;
const width: Size = '100px'; // Valid
const height: Size = '2em'; // Valid
// const invalid: Size = '100'; // Error
Nested template literals:
type DeepKey<T> = T extends object
? {
[K in keyof T & string]: K | `${K}.${DeepKey<T[K]>}`;
}[keyof T & string]
: never;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}
type ConfigKeys = DeepKey<Config>;
// 'database' | 'database.host' | 'database.port' |
// 'database.credentials' | 'database.credentials.username' | ...
Runtime type checking with type narrowing:
// Basic type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Discriminated union guard
interface Success {
status: 'success';
data: string;
}
interface Error {
status: 'error';
message: string;
}
type Result = Success | Error;
function isSuccess(result: Result): result is Success {
return result.status === 'success';
}
function handleResult(result: Result) {
if (isSuccess(result)) {
console.log(result.data); // Type narrowed to Success
} else {
console.log(result.message); // Type narrowed to Error
}
}
Generic type guards:
function isArrayOf<T>(
value: unknown,
check: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(check);
}
const data: unknown = [1, 2, 3];
if (isArrayOf(data, (x): x is number => typeof x === 'number')) {
data.forEach(n => n.toFixed(2)); // Type: number[]
}
Type-safe state machines and variants:
// State machine with exhaustive checking
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; error: Error };
function renderState(state: LoadingState): string {
switch (state.status) {
case 'idle':
return 'Not started';
case 'loading':
return 'Loading...';
case 'success':
return `Loaded ${state.data.length} items`;
case 'error':
return `Error: ${state.error.message}`;
}
// Exhaustiveness checking ensures all cases handled
}
Complex discriminated unions:
// API action types
type Action =
| { type: 'FETCH_USER'; payload: { userId: string } }
| { type: 'UPDATE_USER'; payload: { userId: string; data: Partial<User> } }
| { type: 'DELETE_USER'; payload: { userId: string } }
| { type: 'CLEAR_USERS' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_USER':
// action.payload is { userId: string }
return { ...state, loading: true };
case 'UPDATE_USER':
// action.payload is { userId: string; data: Partial<User> }
return updateUser(state, action.payload);
case 'DELETE_USER':
return deleteUser(state, action.payload.userId);
case 'CLEAR_USERS':
// action has no payload
return { ...state, users: [] };
}
}
Create nominal types for type safety:
// Prevent mixing similar primitive types
type UserId = string & { readonly __brand: 'UserId' };
type PostId = string & { readonly __brand: 'PostId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
function getUser(userId: UserId): User {
// Implementation
}
const userId = createUserId('user-123');
const postId = createPostId('post-456');
getUser(userId); // Valid
// getUser(postId); // Type error: PostId not assignable to UserId
Branded types for validation:
type ValidEmail = string & { readonly __brand: 'ValidEmail' };
type ValidURL = string & { readonly __brand: 'ValidURL' };
function validateEmail(email: string): ValidEmail | null {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email) ? (email as ValidEmail) : null;
}
function sendEmail(to: ValidEmail, subject: string, body: string) {
// Guaranteed to have valid email
}
const email = validateEmail('user@example.com');
if (email) {
sendEmail(email, 'Hello', 'World');
}
Type-safe fluent APIs:
interface QueryBuilder<TSelect = unknown, TWhere = unknown> {
select<T>(): QueryBuilder<T, TWhere>;
where<T>(): QueryBuilder<TSelect, T>;
execute(): TSelect extends unknown ? never : Promise<TSelect[]>;
}
// Usage ensures select() called before execute()
const results = await query
.select<User>()
.where<{ age: number }>()
.execute(); // Type: Promise<User[]>
// query.execute(); // Error: select() not called
Progressive builder types:
interface ConfigBuilder<
THost extends string | undefined = undefined,
TPort extends number | undefined = undefined
> {
host: THost;
port: TPort;
withHost<H extends string>(host: H): ConfigBuilder<H, TPort>;
withPort<P extends number>(port: P): ConfigBuilder<THost, P>;
build: THost extends string
? TPort extends number
? () => { host: THost; port: TPort }
: never
: never;
}
const config = new ConfigBuilder()
.withHost('localhost')
.withPort(3000)
.build(); // Valid
// new ConfigBuilder().build(); // Error: host and port required
Generic constraints and inference:
// Constrain to objects with specific keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // Type: string
// const invalid = getProperty(user, 'invalid'); // Error
// Multiple constraints
function merge<T extends object, U extends object>(
obj1: T,
obj2: U
): T & U {
return { ...obj1, ...obj2 };
}
Higher-kinded types pattern:
// Type-safe data structures
interface Functor<F> {
map<A, B>(fa: F extends { value: any } ? F : never, f: (a: A) => B): any;
}
interface Box<T> {
value: T;
}
const boxFunctor: Functor<Box<any>> = {
map<A, B>(fa: Box<A>, f: (a: A) => B): Box<B> {
return { value: f(fa.value) };
}
};
Combine utility types for complex transformations:
// Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Make specific keys required
type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface User {
id?: number;
name?: string;
email?: string;
}
type UserWithId = RequireKeys<User, 'id'>;
// { id: number; name?: string; email?: string }
// Extract function parameter types
type Parameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never;
function processUser(id: number, name: string): void {}
type ProcessUserParams = Parameters<typeof processUser>;
// [number, string]
Leverage TypeScript's type inference:
// Infer from function implementation
function createAction<T extends string, P>(
type: T,
payload: P
) {
return { type, payload };
}
const action = createAction('UPDATE_USER', { id: 1, name: 'John' });
// Type: { type: 'UPDATE_USER'; payload: { id: number; name: string } }
// Infer generic types from usage
function useState<S>(
initialState: S | (() => S)
): [S, (newState: S) => void] {
// Implementation
}
const [count, setCount] = useState(0); // S inferred as number
const [user, setUser] = useState({ name: 'John' }); // S inferred as { name: string }
Const assertions for literal types:
// Without const assertion
const colors1 = ['red', 'green', 'blue'];
// Type: string[]
// With const assertion
const colors2 = ['red', 'green', 'blue'] as const;
// Type: readonly ['red', 'green', 'blue']
// Narrow object types
const config = {
endpoint: '/api/users',
method: 'GET'
} as const;
// Type: { readonly endpoint: '/api/users'; readonly method: 'GET' }
// Use in discriminated unions
type Action =
| ReturnType<typeof createAction<'INCREMENT'>>
| ReturnType<typeof createAction<'DECREMENT'>>;
Class and method decorators for cross-cutting concerns:
// Method decorator for logging
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = original.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
// Property decorator for validation
function validate(validator: (value: any) => boolean) {
return function(target: any, propertyKey: string) {
let value = target[propertyKey];
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue) => {
if (!validator(newValue)) {
throw new Error(`Invalid value for ${propertyKey}`);
}
value = newValue;
}
});
};
}
class User {
@validate(email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
email: string;
}
Complex type transformations:
// Flatten nested types
type Flatten<T> = T extends any[] ? T[number] : T;
type Nested = (string | number)[][];
type Flat = Flatten<Nested>; // (string | number)[]
// Exclude nullable values
type NonNullable<T> = T extends null | undefined ? never : T;
// Create function overload types
type Overload<T> = T extends {
(...args: infer A1): infer R1;
(...args: infer A2): infer R2;
}
? ((...args: A1) => R1) & ((...args: A2) => R2)
: never;
// Recursive type definitions
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
function parseJSON(json: string): JSONValue {
return JSON.parse(json);
}
Keep types simple and composable:
// Bad - deeply nested types
type Complex<T> = T extends Array<infer U>
? U extends Array<infer V>
? V extends Array<infer W>
? W extends Array<infer X>
? X
: never
: never
: never
: never;
// Good - iterative approach
type ElementType<T> = T extends (infer E)[] ? E : T;
type Deep1<T> = ElementType<T>;
type Deep2<T> = ElementType<Deep1<T>>;
Extract common patterns:
// Define once, reuse everywhere
type ID = string | number;
type Timestamp = number;
type Optional<T> = T | null | undefined;
interface User {
id: ID;
createdAt: Timestamp;
lastLogin: Optional<Timestamp>;
}
Let TypeScript infer when possible:
// Don't over-annotate
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]; // Type inferred automatically
// Use inference in generics
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T inferred as 42 (literal type)
// Bad - unsafe type assertion
const value = input as string;
// Good - safe type guard
function assertString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Not a string');
}
}
assertString(input);
// input is now narrowed to string
// Bad - loses type safety
function process(data: any) {
return data.toUpperCase(); // No type checking
}
// Good - maintains type safety
function processUnknown(data: unknown) {
if (typeof data === 'string') {
return data.toUpperCase(); // Type guard required
}
throw new Error('Expected string');
}
// Bad - unnecessary complexity
function add<T extends number, U extends number>(a: T, b: U): number {
return a + b;
}
// Good - simple and clear
function add(a: number, b: number): number {
return a + b;
}
Use type-level tests:
// Type assertion tests
type AssertEqual<T, U> = T extends U ? (U extends T ? true : false) : false;
type Test1 = AssertEqual<Pick<User, 'name'>, { name: string }>; // true
// Compile-time validation
function expectType<T>(value: T): T {
return value;
}
const user: User = { id: 1, name: 'John', email: 'john@example.com', password: 'secret' };
expectType<UserResponse>(user); // Error: password should not exist