TypeScript Best Practices for 2024
Explore modern TypeScript patterns and best practices. Learn how to write type-safe, maintainable code that scales with your team.
Why TypeScript?
TypeScript has become the de facto standard for building large-scale JavaScript applications. It provides static typing, better tooling, and catches errors before they reach production.
Essential Best Practices
1. Use Strict Mode
Always enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}
2. Prefer Interfaces Over Types
For object shapes, use interfaces:
// Good
interface User {
id: string;
name: string;
email: string;
}
// Use type for unions and advanced types
type Status = 'pending' | 'active' | 'inactive';
3. Use Unknown Over Any
When you don’t know the type, use unknown:
// Avoid
function process(data: any) {
return data.value; // No type checking
}
// Better
function process(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value;
}
}
4. Leverage Utility Types
TypeScript provides powerful utility types:
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}
// Pick specific properties
type TodoPreview = Pick<Todo, 'id' | 'title'>;
// Make all properties optional
type PartialTodo = Partial<Todo>;
// Make all properties readonly
type ReadonlyTodo = Readonly<Todo>;
Advanced Patterns
Generic Constraints
Use constraints to ensure type safety:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // Type: string
Discriminated Unions
Create type-safe state machines:
type State =
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; error: Error };
function handleState(state: State) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return state.data.join(', '); // TypeScript knows data exists
case 'error':
return state.error.message; // TypeScript knows error exists
}
}
Template Literal Types
Create precise string types:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = `/api/${string}`;
type ApiEndpoint = `${HttpMethod} ${Route}`;
// Valid
const endpoint: ApiEndpoint = 'GET /api/users';
Practical Tips
1. Use Type Guards
Create reusable type guards:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processValue(value: unknown) {
if (isString(value)) {
return value.toUpperCase(); // TypeScript knows it's a string
}
}
2. Avoid Type Assertions
Use type narrowing instead:
// Avoid
const element = document.getElementById('myId') as HTMLInputElement;
// Better
const element = document.getElementById('myId');
if (element instanceof HTMLInputElement) {
element.value = 'Hello';
}
3. Use Const Assertions
Create readonly literal types:
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
} as const;
// config.apiUrl is type 'https://api.example.com' not string
Conclusion
TypeScript is a powerful tool that can dramatically improve code quality and developer productivity. By following these best practices, you’ll write more maintainable, type-safe code that scales with your team. Remember: TypeScript is a journey, not a destination. Keep learning and refining your skills!