| name | typescript-patterns |
| description | TypeScript best practices. Use when writing TypeScript code for backend services, API handlers, or shared utilities. Applies async/await patterns, typed errors, and strict type safety. |
TypeScript Best Practices
Patterns
Async/Await Pattern
Always use async/await, never callbacks or .then():
// Good
async function createTask(data: CreateTaskRequest): Promise<Task> {
const task = await db.insert(tasks).values(data).returning();
return task[0];
}
// Bad
function createTask(data: CreateTaskRequest): Promise<Task> {
return db.insert(tasks).values(data).returning().then(result => result[0]);
}
Error Handling
Always use typed errors:
class EntityNotFoundError extends Error {
constructor(entityType: string, id: string | number) {
super(`${entityType} ${id} not found`);
this.name = 'EntityNotFoundError';
}
}
// Usage
async function getTask(id: number): Promise<Task> {
const task = await db.query.tasks.findFirst({ where: eq(tasks.id, id) });
if (!task) throw new EntityNotFoundError('Task', id);
return task;
}
Type Safety
Use strict TypeScript, never 'any':
// Good
interface CreateTaskRequest {
title: string;
description?: string;
epicId?: number;
}
// Bad
function createTask(data: any) { ... }
API Response Pattern
All API responses follow this structure:
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
// Usage
app.get('/tasks/:id', async (req, res) => {
try {
const task = await getTask(Number(req.params.id));
res.json({ success: true, data: task });
} catch (error) {
res.status(404).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
Discriminated Unions
Use discriminated unions for type-safe state handling:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
// Usage
function handleState<T>(state: RequestState<T>) {
switch (state.status) {
case 'idle':
return 'Not started';
case 'loading':
return 'Loading...';
case 'success':
return state.data; // TypeScript knows data exists
case 'error':
return state.error; // TypeScript knows error exists
}
}
Utility Types
Use built-in utility types effectively:
// Partial - all properties optional
type UpdateTaskRequest = Partial<CreateTaskRequest>;
// Pick - select specific properties
type TaskSummary = Pick<Task, 'id' | 'title' | 'status'>;
// Omit - exclude specific properties
type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;
// Required - all properties required
type RequiredConfig = Required<OptionalConfig>;
// Record - typed object with specific keys
type StatusCounts = Record<TaskStatus, number>;
Generics
Use generics for reusable, type-safe functions:
// Generic repository pattern
interface Repository<T, ID> {
findById(id: ID): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: ID): Promise<void>;
}
// Generic API handler
async function handleRequest<T>(
fn: () => Promise<T>,
res: Response
): Promise<void> {
try {
const data = await fn();
res.json({ success: true, data });
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
File Organization
src/
├── types/ # Shared TypeScript types
├── services/ # Business logic
├── routes/ # API endpoints
├── db/ # Database schema & migrations
└── utils/ # Helper functions
Import Best Practices
// Good - explicit imports
import { Task, TaskStatus } from './types';
import { createTask, updateTask } from './services/taskService';
// Bad - wildcard imports
import * as types from './types';
import * as taskService from './services/taskService';
Null Handling
// Use nullish coalescing
const name = user.name ?? 'Anonymous';
// Use optional chaining
const city = user.address?.city;
// Type guards for narrowing
function isTask(obj: unknown): obj is Task {
return typeof obj === 'object' && obj !== null && 'id' in obj && 'title' in obj;
}