Loading lesson path
Concept visual
Start at both ends
This guide covers essential TypeScript best practices to help you write clean, maintainable, and type-safe code. Following these practices will improve code quality and developer experience.
Always enable strict mode in your tsconfig.json for maximum type safety: // tsconfig.json
{
"compilerOptions": {Formula
/* Enable all strict type - checking options */"strict": true, /* Additional recommended settings */ "target": "ES2020", "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true
}
}Consider enabling these additional strict checks for better code quality:
{
"compilerOptions": {/* Additional strict checks */ "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true
}
}Let TypeScript infer types when the type is obvious from the assignment: // Bad: Redundant type annotation const name: string = 'John'; // Good: Let TypeScript infer the type const name = 'John'; // Bad: Redundant return type function add(a: number, b: number): number {
return a + b;
}
// Good: Let TypeScript infer return type function add(a: number, b: number) {
return a + b;
}Be explicit with types for public APIs and function parameters: // Bad: No type information function processUser(user) {
return user.name.toUpperCase();
}// Good: Explicit parameter and return types interface User {
id: number;
name: string;
email?: string; // Optional property
}
function processUser(user: User): string {
return user.name.toUpperCase();
}Interfaces vs. Type Aliases
// Use interface for object shapes that can be extended/implemented interface User {
id: number;
name: string;
}
// Extending an interface interface AdminUser extends User {
permissions: string[];
}
// Use type for unions, tuples, or mapped types type UserRole = 'admin' | 'editor' | 'viewer';
// Union types type UserId = number | string;
// Mapped types type ReadonlyUser = Readonly<User>;
// Tuple types type Point = [number, number];// Bad: Loses type safety function logValue(value: any) {
console.log(value.toUpperCase()); // No error until runtime
}Formula
// Better: Use generic type parameter function logValue < T > (value: T) {console.log(String(value)); // Safer, but still not ideal
}
// Best: Be specific about expected types function logString(value: string) {
console.log(value.toUpperCase()); // Type-safe
}Formula
// When you need to accept any value but still be type - safe function logUnknown(value: unknown) {if (typeof value === 'string') {
console.log(value.toUpperCase());
} else {
console.log(String(value));
}
}Organize code into logical modules with clear responsibilities:
// user/user.model.ts export interface User {
id: string;
name: string;
email: string;
}
// user/user.service.ts import { User } from './user.model';
export class UserService {
private users: User[] = [];
addUser(user: User) {
this.users.push(user);
}
getUser(id: string): User | undefined {
return this.users.find(user => user.id === id);
}
}Formula
// user/index.ts (barrel file)export * from './user.model';
export * from './user.service';// Good user.service.ts // Service classes user.model.ts // Type definitions user.controller.ts // Controllers user.component.ts // Components user.utils.ts // Utility functions user.test.ts // Test files // Bad UserService.ts // Avoid PascalCase for file names user_service.ts // Avoid snake_case userService.ts // Avoid camelCase for file names
Document your types and interfaces. Prefer composition over inheritance for types.
Formula
Keep tsconfig.json strict and up - to - date.Refactor code to use more specific types as the codebase evolves.
Formula
Write clear and type - safe functions with proper parameter and return types:// Bad: No type information function process(user, notify) {
notify(user.name);
}
// Good: Explicit parameter and return types function processUser(
user: User, notify: (message: string) => void
): void {
notify(`Processing user: ${user.name}`);
}
// Use default parameters instead of conditionals function createUser(Formula
name: string, role: UserRole = 'viewer', isActive: boolean = true): User {
return { name, role, isActive };
}
// Use rest parameters for variable arguments function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}Be mindful of function complexity and responsibilities: // Bad: Too many responsibilities function processUserData(userData: any) {
// Validation if (!userData || !userData.name) throw new Error('Invalid user data');
// Data transformation const processedData = {...userData, name: userData.name.trim(), createdAt: new Date()
};
// Side effect saveToDatabase(processedData);
// Notification sendNotification(processedData.email, 'Profile updated');
return processedData;
}
// Better: Split into smaller, focused functions function validateUserData(data: unknown): UserData {
if (!data || typeof data !== 'object') {
throw new Error('Invalid user data');
}
return data as UserData;
}
function processUserData(userData: UserData): ProcessedUserData {
return {...userData, name: userData.name.trim(), createdAt: new Date()
};
}Handle asynchronous operations effectively with proper error handling: // Bad: Not handling errors async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}Formula
// Good: Proper error handling async function fetchData < T > (url: string): Promise < T > {try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json() as T;
} catch (error) {
console.error('Failed to fetch data:', error);
throw error; // Re-throw to allow caller to handle
}
}Formula
// Better: Use Promise.all for parallel operations async function fetchMultipleData < T > (urls: string[]): Promise < T[]> {try {
const promises = urls.map(url => fetchData<T>(url));
return await Promise.all(promises);
} catch (error) {
console.error('One or more requests failed:', error);
throw error;
}
}
// Example usage interface User {
id: string;
name: string;
email: string;
}Formula
// Fetch user data with proper typing async function getUserData(userId: string): Promise < User > {return fetchData<User>(`/api/users/${userId}`);
}Formula
Flatten your async/await code to avoid callback hell:
// Bad: Nested async/await (callback hell)async function processUser(userId: string) {
const user = await getUser(userId);
if (user) {
const orders = await getOrders(user.id);
if (orders.length > 0) {
const latestOrder = orders[0];
const items = await getOrderItems(latestOrder.id);
return { user, latestOrder, items };
}
}
return null;
}Formula
// Better: Flatten the async/await chain async function processUser(userId: string) {const user = await getUser(userId);
if (!user) return null;
const orders = await getOrders(user.id);
if (orders.length === 0) return { user, latestOrder: null, items: [] };
const latestOrder = orders[0];
const items = await getOrderItems(latestOrder.id);
return { user, latestOrder, items };
}
// Best: Use Promise.all for independent async operations async function processUser(userId: string) {
const [user, orders] = await Promise.all([
getUser(userId), getOrders(userId)
]);
if (!user) return null;
if (orders.length === 0) return { user, latestOrder: null, items: [] };
const latestOrder = orders[0];
const items = await getOrderItems(latestOrder.id);
return { user, latestOrder, items };
}Design your code with testability in mind by using dependency injection and pure functions: // Bad: Hard to test due to direct dependencies class PaymentProcessor {
async processPayment(amount: number) {
const paymentGateway = new PaymentGateway();
return paymentGateway.charge(amount);
}
}// Better: Use dependency injection interface PaymentGateway {
charge(amount: number): Promise<boolean>;
}
class PaymentProcessor {
constructor(private paymentGateway: PaymentGateway) {}
async processPayment(amount: number): Promise<boolean> {
if (amount <= 0) {
throw new Error('Amount must be greater than zero');
}
return this.paymentGateway.charge(amount);
}
}
// Test example with Jest describe('PaymentProcessor', () => {
let processor: PaymentProcessor;
let mockGateway: jest.Mocked<PaymentGateway>;
beforeEach(() => {
mockGateway = {
charge: jest.fn()
};
processor = new PaymentProcessor(mockGateway);
});
it('should process a valid payment', async () => {
mockGateway.charge.mockResolvedValue(true);
const result = await processor.processPayment(100);
expect(result).toBe(true);
expect(mockGateway.charge).toHaveBeenCalledWith(100);
});
it('should throw for invalid amount', async () => {
await expect(processor.processPayment(-50)).rejects toThrow('Amount must be greater than zero');
});
});