TypeScriptClean CodeBest Practices
TypeScript Patterns for Clean Code
·10 min read
TypeScript Patterns for Clean Code
TypeScript's type system is incredibly powerful, but it takes practice to use it effectively. Here are patterns I've learned from working on large-scale applications.
Discriminated Unions for State Management
Instead of optional properties, use discriminated unions:
// Bad: Optional properties make state unclear
interface ApiResponse {
data?: User;
error?: Error;
loading?: boolean;
}
// Good: Discriminated union makes state explicit
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleState(state: ApiState<User>) {
switch (state.status) {
case 'idle':
return <Placeholder />;
case 'loading':
return <Spinner />;
case 'success':
return <UserCard user={state.data} />;
case 'error':
return <ErrorMessage error={state.error} />;
}
}Branded Types for Type Safety
Create distinct types for values that are structurally identical:
type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
const userId = createUserId('user-123');
const orderId = createOrderId('order-456');
getUser(userId); // OK
getUser(orderId); // Error! OrderId is not assignable to UserIdBuilder Pattern with Method Chaining
class QueryBuilder<T> {
private query: Partial<Query> = {};
select<K extends keyof T>(...fields: K[]): this {
this.query.select = fields;
return this;
}
where(condition: Partial<T>): this {
this.query.where = condition;
return this;
}
orderBy(field: keyof T, direction: 'asc' | 'desc' = 'asc'): this {
this.query.orderBy = { field, direction };
return this;
}
limit(count: number): this {
this.query.limit = count;
return this;
}
build(): Query {
return this.query as Query;
}
}
// Usage
const query = new QueryBuilder<User>()
.select('id', 'name', 'email')
.where({ active: true })
.orderBy('createdAt', 'desc')
.limit(10)
.build();Conclusion
These patterns have helped me write more maintainable TypeScript code. The key is to leverage the type system to make invalid states unrepresentable.