TypeScript has become the standard for building large-scale JavaScript applications. However, writing TypeScript code and writing good TypeScript code are two different things. This guide covers advanced patterns and practices for enterprise-level TypeScript development.
Type Safety Fundamentals
Strict Mode Configuration
Always enable strict mode in your tsconfig.json. This includes:
strictNullChecks: Prevents null/undefined errorsstrictFunctionTypes: Ensures function type safetynoImplicitAny: Requires explicit typesnoUnusedLocals: Catches unused variables
Discriminated Unions
Use discriminated unions for type-safe state management and API responses:
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string };
function handleResponse<T>(response: ApiResponse<T>) {
if (response.status === 'success') {
return response.data; // TypeScript knows data exists
}
throw new Error(response.message);
}
Advanced Type Patterns
Utility Types
Leverage TypeScript's built-in utility types:
Partial<T>: Makes all properties optionalRequired<T>: Makes all properties requiredPick<T, K>: Select specific propertiesOmit<T, K>: Exclude specific propertiesRecord<K, V>: Create object types
Template Literal Types
Use template literal types for type-safe string manipulation:
type EventName = 'click' | 'hover' | 'focus';
type HandlerName = `on${Capitalize<EventName>}`;
// Result: 'onClick' | 'onHover' | 'onFocus'
Design Patterns
Repository Pattern
Use interfaces and generics to create type-safe repositories:
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: ID, entity: Partial<T>): Promise<T>;
delete(id: ID): Promise<void>;
}
Builder Pattern
TypeScript's type system makes the builder pattern more powerful with method chaining and type inference.
Error Handling
Create custom error classes with proper typing:
class ApiError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string
) {
super(message);
this.name = 'ApiError';
}
}
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
Code Organization
Barrel Exports
Use index.ts files to create clean public APIs for your modules. This improves import statements and makes refactoring easier.
Feature-Based Structure
Organize code by features rather than by technical layers. Each feature module should be self-contained with its own types, components, and logic.
Testing with TypeScript
TypeScript improves testing by catching errors at compile time. Use type-safe test utilities and mock factories to ensure your tests are maintainable.
Performance Considerations
While TypeScript adds compile-time overhead, it has zero runtime cost. However, be mindful of:
- Avoid excessive type computations in hot paths
- Use type assertions sparingly
- Leverage const assertions for better inference
Real-World Example
In a recent enterprise project, we refactored a 50,000+ line JavaScript codebase to TypeScript. The results:
- 40% reduction in runtime errors
- Improved developer productivity with better IDE support
- Easier onboarding for new team members
- Better code documentation through types
Conclusion
TypeScript is more than just JavaScript with types. By following these best practices and leveraging advanced type features, you can build maintainable, scalable enterprise applications. The investment in proper typing pays off through reduced bugs, better developer experience, and easier maintenance.