TypeScript Best Practices for 2024

TypeScript has become an essential tool for JavaScript developers, providing type safety and improved developer experience. Here are the best practices that will help you write better TypeScript code in 2024.

Strict Configuration

Always start with a strict TypeScript configuration:

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

Type-First Development

Define your types before implementing functionality:

// Define the shape first
interface UserProfile {
  id: string;
  name: string;
  email: string;
  preferences: UserPreferences;
}

interface UserPreferences {
  theme: 'light' | 'dark';
  notifications: boolean;
  language: string;
}

// Then implement
class UserService {
  async getProfile(id: string): Promise<UserProfile> {
    // Implementation follows the type contract
  }
}

Utility Types

Leverage TypeScript’s built-in utility types:

// Pick specific properties
type PublicUserInfo = Pick<UserProfile, 'name' | 'id'>;

// Make properties optional
type PartialUpdate = Partial<UserProfile>;

// Exclude properties
type UserWithoutId = Omit<UserProfile, 'id'>;

// Create unions from object keys
type PreferenceKeys = keyof UserPreferences;

Generic Constraints

Use generic constraints to create flexible yet safe APIs:

interface Identifiable {
  id: string;
}

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

Discriminated Unions

Use discriminated unions for type-safe state management:

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

type SuccessState = {
  status: 'success';
  data: UserProfile[];
};

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 data is available
      console.log(state.data);
      break;
    case 'error':
      // TypeScript knows error is available
      console.error(state.error);
      break;
  }
}

Advanced Patterns

Template Literal Types

type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// Result: 'onClick' | 'onFocus' | 'onBlur'

Conditional Types

type ApiResponse<T> = T extends string 
  ? { message: T } 
  : { data: T };

Conclusion

These TypeScript best practices will help you write more maintainable, type-safe code. Remember that TypeScript is a tool to help you catch errors early and improve code documentation through types.