TypeScript Best Practices for Modern Web Development

Essential TypeScript patterns and practices for building maintainable applications

TypeScript Best Practices for Modern Web Development

TypeScript has become the de facto standard for large-scale JavaScript applications. Here are some essential best practices to help you write better TypeScript code.

1. Use Strict Mode

Always enable strict mode in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

2. Prefer Type Inference

Let TypeScript infer types when possible:

// Good - type is inferred
const users = ['Alice', 'Bob', 'Charlie'];

// Unnecessary - type annotation is redundant
const users: string[] = ['Alice', 'Bob', 'Charlie'];

3. Use Union Types Effectively

Union types are powerful for modeling data that can be one of several types:

type Status = 'loading' | 'success' | 'error';

interface ApiResponse {
  status: Status;
  data?: any;
  error?: string;
}

4. Leverage Utility Types

TypeScript provides many built-in utility types:

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// Create a type with only some properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties required
type RequiredUser = Required<User>;

5. Use Discriminated Unions

For complex state management, discriminated unions are invaluable:

type LoadingState = {
  status: 'loading';
};

type SuccessState = {
  status: 'success';
  data: any;
};

type ErrorState = {
  status: 'error';
  error: string;
};

type AppState = LoadingState | SuccessState | ErrorState;

function handleState(state: AppState) {
  switch (state.status) {
    case 'loading':
      // TypeScript knows this is LoadingState
      break;
    case 'success':
      // TypeScript knows this is SuccessState
      console.log(state.data);
      break;
    case 'error':
      // TypeScript knows this is ErrorState
      console.error(state.error);
      break;
  }
}

6. Generic Constraints

Use generic constraints to make your functions more flexible yet type-safe:

interface Identifiable {
  id: number;
}

function updateEntity<T extends Identifiable>(
  entity: T,
  updates: Partial<T>
): T {
  return { ...entity, ...updates };
}

Conclusion

These practices will help you write more maintainable and type-safe TypeScript code. Remember, the goal is to leverage TypeScript’s type system to catch errors at compile time and improve developer experience.