TypeScript Best Practices for Large Projects
Introduction
TypeScript has become the standard for enterprise JavaScript development. This guide covers advanced patterns, configuration strategies, and best practices for building maintainable large-scale applications.
1. Strict TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"esModuleInterop": true,
"module": "esnext",
"target": "es2022",
"lib": ["es2022", "dom"],
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": false,
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "build"]
}
2. Type-Safe API Responses
// types/api.ts
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
// api/users.ts
async function getUser(id: string): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data;
}
// Type guard
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
3. Advanced Type Patterns
Utility Types
// Make all properties optional recursively
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Extract function return type
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never;
// Require at least one property
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys];
// Usage
interface SearchParams {
query?: string;
category?: string;
tags?: string[];
}
type ValidSearch = RequireAtLeastOne<SearchParams>;
Discriminated Unions
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function handleResult<T>(result: Result<T>) {
if (result.success) {
console.log(result.data); // TypeScript knows data exists
} else {
console.error(result.error); // TypeScript knows error exists
}
}
4. Generic Constraints
// Constrain to objects with id property
interface HasId {
id: string | number;
}
function findById<T extends HasId>(items: T[], id: string | number): T | undefined {
return items.find(item => item.id === id);
}
// Multiple constraints
interface Nameable {
name: string;
}
function sortByName<T extends HasId & Nameable>(items: T[]): T[] {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}
5. Mapped Types & Template Literals
// Create getter methods for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
// Event system with type safety
type EventMap = {
'user:login': { userId: string; timestamp: number };
'user:logout': { userId: string };
'data:update': { data: any };
};
class TypedEventEmitter {
on<K extends keyof EventMap>(
event: K,
handler: (payload: EventMap[K]) => void
) {
// Implementation
}
emit<K extends keyof EventMap>(
event: K,
payload: EventMap[K]
) {
// Implementation
}
}
6. Conditional Types
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : T;
// Function parameter types
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// Exclude null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// Practical example
type ApiRoutes = {
'/users': User[];
'/posts': Post[];
'/comments': Comment[];
};
type ApiResponse<T extends keyof ApiRoutes> = {
data: ApiRoutes[T];
timestamp: number;
};
async function fetchApi<T extends keyof ApiRoutes>(
route: T
): Promise<ApiResponse<T>> {
const response = await fetch(route);
return response.json();
}
7. Branded Types for Type Safety
// Prevent mixing different ID types
type UserId = string & { __brand: 'UserId' };
type PostId = string & { __brand: 'PostId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
function getUser(id: UserId) {
// Implementation
}
const userId = createUserId('123');
const postId = createPostId('456');
getUser(userId); // ✓ OK
// getUser(postId); // ✗ Error: Type 'PostId' is not assignable to type 'UserId'
8. Readonly and Immutability
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface Config {
api: {
baseUrl: string;
timeout: number;
};
features: string[];
}
const config: DeepReadonly<Config> = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
},
features: ['auth', 'analytics']
};
// config.api.baseUrl = 'new-url'; // Error: readonly
// config.features.push('new'); // Error: readonly
9. Error Handling Patterns
// Custom error types
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: unknown
) {
super(message);
this.name = 'ValidationError';
}
}
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = 'NetworkError';
}
}
// Type-safe error handling
type AppError = ValidationError | NetworkError;
function handleError(error: AppError) {
if (error instanceof ValidationError) {
console.error(`Validation failed for ${error.field}`);
} else if (error instanceof NetworkError) {
console.error(`Network error: ${error.statusCode}`);
}
}
10. Module Augmentation
// Extend third-party types
declare module 'express' {
interface Request {
user?: {
id: string;
role: string;
};
}
}
// Usage in Express
app.get('/profile', (req, res) => {
const userId = req.user?.id; // TypeScript knows about user property
});
11. Testing with TypeScript
// Type-safe mocks
interface UserService {
getUser(id: string): Promise<User>;
createUser(data: CreateUserDto): Promise<User>;
deleteUser(id: string): Promise<void>;
}
type MockUserService = {
[K in keyof UserService]: jest.Mock<
ReturnType<UserService[K]>,
Parameters<UserService[K]>
>;
};
const mockUserService: MockUserService = {
getUser: jest.fn(),
createUser: jest.fn(),
deleteUser: jest.fn()
};
// Type-safe test data factories
function createMockUser(overrides?: Partial<User>): User {
return {
id: '1',
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides
};
}
12. Performance Optimization
// Use const assertions for literal types
const ROUTES = {
HOME: '/',
USERS: '/users',
POSTS: '/posts'
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/users' | '/posts'
// Avoid expensive type operations in hot paths
// BAD: Complex mapped type in component props
type BadProps = {
[K in string]: ComplexType<K>;
};
// GOOD: Pre-compute types
type GoodProps = PreComputedType;
// Use satisfies operator (TypeScript 4.9+)
const config = {
url: 'https://api.example.com',
timeout: 5000,
retry: true
} satisfies Config; // Validates but preserves literal types
💡 Best Practices Summary:
- Enable strict mode and all strict flags
- Use discriminated unions for complex state
- Prefer type inference over explicit types when obvious
- Create branded types for domain-specific values
- Use const assertions for immutable data
- Implement proper error types hierarchy
- Leverage utility types and conditional types
- Document complex types with JSDoc comments
- Use path aliases for cleaner imports
- Regular code reviews focusing on type safety
Conclusion
TypeScript's type system is incredibly powerful when used correctly. By following these patterns and best practices, you can build robust, maintainable applications that catch errors at compile time and provide excellent developer experience through intelligent autocomplete and refactoring support.
Remember: TypeScript is a tool to help you write better JavaScript. Don't fight the type system – embrace it and let it guide you toward better code architecture.