TypeScript Best Practices: Beyond the Basics


Disclosure: This article may contain affiliate links. We only recommend products we believe in.

Stop Using any

Every any is a hole in your type system. It disables all type checking for that value and everything it touches. Use unknown instead when you genuinely do not know the type, and narrow it with type guards.

// Bad
function processData(data: any) {
  return data.name.toUpperCase(); // No error, will crash at runtime if data has no name
}

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

Discriminated Unions for State

type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: string };

function renderUsers(state: RequestState) {
  switch (state.status) {
    case 'idle': return 'Click to load';
    case 'loading': return 'Loading...';
    case 'success': return state.data.map(u => u.name); // TypeScript knows data exists
    case 'error': return `Error: ${state.error}`; // TypeScript knows error exists
  }
}

Const Assertions

// Without const assertion: type is string[]
const colors = ['red', 'green', 'blue'];

// With const assertion: type is readonly ['red', 'green', 'blue']
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'

Utility Types You Should Know

  • Partial<T>: All properties optional (useful for update operations)
  • Required<T>: All properties required
  • Pick<T, K>: Select specific properties
  • Omit<T, K>: Exclude specific properties
  • Record<K, V>: Object with specific key and value types
  • ReturnType<T>: Extract function return type

Zod for Runtime Validation

TypeScript types disappear at runtime. For API boundaries, user input, and external data, use Zod to validate at runtime AND infer TypeScript types:

For more on this topic, see our guide on API Rate Limiting: Implementation Strategies and Best Practices.

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive(),
});

type User = z.infer<typeof UserSchema>; // TypeScript type derived from schema

const result = UserSchema.safeParse(apiResponse);
if (result.success) {
  // result.data is fully typed as User
}