| name | typescript-patterns |
| description | Modern TypeScript patterns for type-safe, maintainable code. Use when designing interfaces, working with generics, or implementing type utilities. Triggers on TypeScript, type, interface, generic, discriminated union, type guard, infer, utility type. |
TypeScript Best Practices
Modern TypeScript patterns for type-safe, maintainable code with maximum developer experience.
When to Use This Skill
- Designing interfaces and types
- Working with generics
- Creating type-safe APIs
- Implementing type guards
- Building utility types
Core Principles
Use TypeScript Strict Mode
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Prefer interface for Objects
interface Task {
id: string;
title: string;
status: TaskStatus;
}
type TaskStatus = 'pending' | 'active' | 'completed';
type TaskId = string;
type TaskRecord = Record<string, Task>;
interface TimestampedTask extends Task {
createdAt: Date;
updatedAt: Date;
}
Make Invalid States Unrepresentable
interface Session {
status: 'idle' | 'running' | 'completed' | 'error';
startedAt?: Date;
completedAt?: Date;
error?: Error;
result?: SessionResult;
}
type Session =
| { status: 'idle' }
| { status: 'running'; startedAt: Date }
| { status: 'completed'; startedAt: Date; completedAt: Date; result: SessionResult }
| { status: 'error'; startedAt: Date; error: Error };
function handleSession(session: Session) {
switch (session.status) {
case 'idle':
return 'Ready to start';
case 'running':
return `Running since ${session.startedAt.toISOString()}`;
case 'completed':
return `Completed: ${session.result.summary}`;
case 'error':
return `Error: ${session.error.message}`;
}
}
Type Guards
Custom Type Guards
function isTask(value: unknown): value is Task {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'title' in value &&
'status' in value &&
typeof (value as Task).id === 'string' &&
typeof (value as Task).title === 'string'
);
}
function processItem(item: unknown) {
if (isTask(item)) {
console.log(item.title);
}
}
Assertion Functions
function assertTask(value: unknown): asserts value is Task {
if (!isTask(value)) {
throw new Error('Invalid task data');
}
}
function processResponse(data: unknown) {
assertTask(data);
return data.title;
}
Discriminated Union Type Guards
type AgentStep =
| { type: 'text'; content: string }
| { type: 'tool_call'; toolName: string; args: Record<string, unknown> }
| { type: 'tool_result'; toolName: string; result: unknown };
function isToolCall(step: AgentStep): step is Extract<AgentStep, { type: 'tool_call' }> {
return step.type === 'tool_call';
}
function handleStep(step: AgentStep) {
if (isToolCall(step)) {
executeToolCall(step.toolName, step.args);
}
}
Generics
Basic Generic Functions
function first<T>(items: T[]): T | undefined {
return items[0];
}
const task = first(tasks);
const num = first([1, 2, 3]);
Constrained Generics
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const task = findById(tasks, 'task-1');
const user = findById(users, 'user-1');
Generic Interfaces
interface Result<T, E = Error> {
success: boolean;
data?: T;
error?: E;
}
function success<T>(data: T): Result<T> {
return { success: true, data };
}
function failure<E = Error>(error: E): Result<never, E> {
return { success: false, error };
}
async function fetchTask(id: string): Promise<Result<Task>> {
try {
const task = await taskService.get(id);
return success(task);
} catch (e) {
return failure(e as Error);
}
}
Utility Types
Built-in Utility Types
interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
priority: number;
createdAt: Date;
}
type TaskUpdate = Partial<Task>;
type TaskSummary = Pick<Task, 'id' | 'title' | 'status'>;
type CreateTaskInput = Omit<Task, 'id' | 'createdAt'>;
type RequiredTask = Required<Task>;
type TasksByStatus = Record<TaskStatus, Task[]>;
type ImmutableTask = Readonly<Task>;
Custom Utility Types
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
type TaskWithTitle = RequireKeys<Partial<Task>, 'title'>;
type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type TaskOptionalDescription = OptionalKeys<Task, 'description'>;
type NonNullableFields<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
Template Literal Types
type EventName<T extends string> = `on${Capitalize<T>}`;
type TaskEventName = EventName<'create' | 'update' | 'delete'>;
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route<M extends Method, P extends string> = `${M} ${P}`;
type TaskRoutes =
| Route<'GET', '/tasks'>
| Route<'GET', '/tasks/:id'>
| Route<'POST', '/tasks'>
| Route<'PUT', '/tasks/:id'>
| Route<'DELETE', '/tasks/:id'>;
Const Assertions
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
};
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const;
const TaskStatus = {
PENDING: 'pending',
ACTIVE: 'active',
COMPLETED: 'completed',
} as const;
type TaskStatus = typeof TaskStatus[keyof typeof TaskStatus];
Function Overloads
function createTask(title: string): Task;
function createTask(input: CreateTaskInput): Task;
function createTask(titleOrInput: string | CreateTaskInput): Task {
if (typeof titleOrInput === 'string') {
return {
id: generateId(),
title: titleOrInput,
status: 'pending',
priority: 3,
createdAt: new Date(),
};
}
return {
id: generateId(),
...titleOrInput,
createdAt: new Date(),
};
}
const task1 = createTask('Quick task');
const task2 = createTask({ title: 'Detailed task', priority: 1 });
Branded Types
declare const brand: unique symbol;
type Brand<T, B> = T & { [brand]: B };
type TaskId = Brand<string, 'TaskId'>;
type UserId = Brand<string, 'UserId'>;
function getTask(id: TaskId): Task { }
function getUser(id: UserId): User { }
function taskId(id: string): TaskId {
return id as TaskId;
}
function userId(id: string): UserId {
return id as UserId;
}
const tId = taskId('task-1');
const uId = userId('user-1');
getTask(tId);
getTask(uId);
Inference with infer
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Awaited<T> = T extends Promise<infer U> ? U : T;
type ElementOf<T> = T extends (infer E)[] ? E : never;
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type TaskServiceReturn = ReturnTypeOf<typeof taskService.getAll>;
type FetchedTask = Awaited<ReturnType<typeof fetchTask>>;
type SingleTask = ElementOf<Task[]>;
Best Practices Summary
| Do | Don't |
|---|
Use interface for objects | Use type for everything |
| Use discriminated unions | Use optional properties for state |
| Enable strict mode | Disable for convenience |
| Use type guards | Use type assertions (as) |
| Make invalid states unrepresentable | Allow ambiguous states |
| Use generics for reusable code | Duplicate type definitions |
Prefer unknown over any | Use any to bypass checks |
| Use const assertions for literals | Let types widen unnecessarily |