TypeScript Best Practices for Modern Development

Learn essential TypeScript patterns and practices that will make your code more maintainable and robust.

#typescript#javascript#best-practices#development
TypeScript Best Practices for Modern Development

TypeScript Best Practices for Modern Development

TypeScript has become an essential tool in modern JavaScript development, providing static type checking that catches errors early and improves code quality. Here are some best practices I’ve learned from years of TypeScript development.

1. Use Strict Mode

Always enable strict mode in your tsconfig.json to catch more potential issues:

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

2. Leverage Union Types and Type Guards

Union types provide flexibility while maintaining type safety:

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

function handleStatus(status: Status) {
  switch (status) {
    case 'loading':
      return 'Loading...';
    case 'success':
      return 'Success!';
    case 'error':
      return 'Error occurred';
    default:
      // TypeScript will ensure this is never reached
      const exhaustiveCheck: never = status;
      return exhaustiveCheck;
  }
}

3. Use Generic Types Effectively

Generics make your code reusable while preserving type information:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  return response.json();
}

// Usage with proper typing
const userResponse = await fetchData<User>('/api/users/1');
// userResponse.data is typed as User

4. Utility Types Are Your Friend

TypeScript provides many utility types that can save you time:

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// Create a type for updating users (all fields optional)
type UpdateUser = Partial<User>;

// Create a type for user creation (omit id and createdAt)
type CreateUser = Omit<User, 'id' | 'createdAt'>;

// Pick only specific fields
type UserSummary = Pick<User, 'id' | 'name'>;

5. Prefer Interfaces Over Types for Object Shapes

While both interfaces and types can define object shapes, interfaces are generally preferred:

// ✅ Good - use interface for object shapes
interface User {
  id: number;
  name: string;
}

// ✅ Good - interfaces can be extended
interface AdminUser extends User {
  permissions: string[];
}

// ✅ Good - use type for unions, primitives, and computed types
type Theme = 'light' | 'dark';
type EventHandler<T> = (event: T) => void;

6. Use Branded Types for Better Type Safety

Branded types help prevent mixing up similar primitive types:

type UserId = string & { readonly brand: unique symbol };
type Email = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!email.includes('@')) {
    throw new Error('Invalid email');
  }
  return email as Email;
}

// This prevents accidentally mixing up user IDs and emails
function getUser(userId: UserId): User {
  // Implementation
}

const id = createUserId('123');
const email = createEmail('user@example.com');

getUser(id); // ✅ Works
getUser(email); // ❌ TypeScript error

7. Async/Await with Proper Error Handling

Use proper typing for async operations:

type Result<T, E = Error> = {
  success: true;
  data: T;
} | {
  success: false;
  error: E;
};

async function safeApiCall<T>(
  apiCall: () => Promise<T>
): Promise<Result<T>> {
  try {
    const data = await apiCall();
    return { success: true, data };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error : new Error('Unknown error') 
    };
  }
}

// Usage
const result = await safeApiCall(() => fetchUser(userId));
if (result.success) {
  console.log(result.data); // Typed as User
} else {
  console.error(result.error); // Typed as Error
}

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 your development experience. Don’t fight the type system—embrace it and let it guide you toward better code architecture.

The key is to start simple and gradually adopt more advanced patterns as your understanding grows. TypeScript’s power lies not just in catching errors, but in making your code more self-documenting and easier to refactor.

Back to Blog