Who is Eric Su?

TypeScript Best Practices for Modern Development

3 min read
Eric Su
typescriptjavascriptbest-practices

TypeScript Best Practices for Modern Development

TypeScript has become the de facto standard for building large-scale JavaScript applications. Here are some best practices to help you leverage TypeScript effectively.

Use Strict Mode

Always enable strict mode in your tsconfig.json:

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

This catches more potential errors at compile time and enforces better coding practices.

Prefer Interfaces Over Type Aliases for Objects

While both work, interfaces are generally preferred for object shapes:

// Good
interface User {
  id: string;
  name: string;
  email: string;
}

// Also fine, but interfaces are more extensible
type User = {
  id: string;
  name: string;
  email: string;
}

Use Union Types Instead of Enums

Union types are more flexible and tree-shakeable:

// Instead of enum
type Status = 'pending' | 'approved' | 'rejected';

// You can still get autocomplete and type safety
function handleStatus(status: Status) {
  // TypeScript knows the exact values
}

Leverage Type Guards

Type guards help TypeScript narrow down types:

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'name' in obj
  );
}

if (isUser(data)) {
  // TypeScript knows data is User here
  console.log(data.name);
}

Use Utility Types

TypeScript provides helpful utility types:

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

// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Make properties readonly
type ImmutableUser = Readonly<User>;

// Exclude properties
type UserWithoutEmail = Omit<User, 'email'>;

Avoid any at All Costs

Use unknown instead of any when you don't know the type:

// Bad
function processData(data: any) {
  return data.value; // No type checking
}

// Good
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return data.value;
  }
  throw new Error('Invalid data');
}

Use Discriminated Unions

Discriminated unions are powerful for modeling complex state:

type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success'; data: User };
type ErrorState = { status: 'error'; error: string };

type AsyncState = LoadingState | SuccessState | ErrorState;

function renderState(state: AsyncState) {
  switch (state.status) {
    case 'loading':
      return 'Loading...';
    case 'success':
      return state.data.name; // TypeScript knows data exists
    case 'error':
      return state.error; // TypeScript knows error exists
  }
}

Conclusion

These best practices will help you write more maintainable, type-safe TypeScript code. Remember, TypeScript's power comes from its type system—use it to your advantage!