typescript

TypeScript patterns covering strict types, generics constraints, utility types (Pick, Omit, Record, Extract), discriminated unions, async patterns, and common mistakes with BAD/GOOD comparisons.

Types and Type Safety

BAD: Using any defeats the type system.

function processData(data: any) {
  return data.value.toUpperCase();
}

GOOD: Define explicit types or use generics.

interface DataWithValue {
  value: string;
}

function processData(data: DataWithValue): string {
  return data.value.toUpperCase();
}

When TypeScript Can Infer

BAD: Redundant type annotations.

const count: number = 42;
const name: string = 'Alice';

GOOD: Let TypeScript infer.

const count = 42;       // inferred as number
const name = 'Alice';   // inferred as string

Record vs Object

BAD: Using object or {}. These allow any object.

GOOD: Use Record<PropertyKey, unknown> for arbitrary objects.

function logData(data: Record<PropertyKey, unknown>) {
  console.log(data);
}

Interfaces vs Types

GOOD: Use interface for object shapes. Use type for unions, intersections, and primitives.

// Object shapes: interface
interface User {
  id: number;
  name: string;
}

// Unions: type
type Status = 'pending' | 'approved' | 'rejected';

// Intersections: type
type AdminUser = User & { role: 'admin' };

Generics Constraints

BAD: Unconstrained generics allow any type.

function getProperty<T>(obj: T, key: string) {
  return obj[key];  // Error: key is not guaranteed to exist on T
}

GOOD: Constrain generics with extends.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: 'Alice' };
const name = getProperty(user, 'name');  // Type: string

Multiple Constraints

function findById<T extends Identifiable & Named>(
  items: T[],
  id: number
): T | undefined {
  return items.find(item => item.id === id);
}

Default Generic Types

function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

Utility Types

Pick, Omit, Partial, Required, Record, Extract, Exclude, ReturnType. Full reference with BAD/GOOD pairs for each.

See references/utility-types.md for the complete utility types reference.

Discriminated Unions

BAD: Unions without discriminator. TypeScript cannot narrow types.

type Result = { data: string } | { error: Error };

GOOD: Add a discriminator property.

type Success = { status: 'success'; data: string };
type Failure = { status: 'error'; error: Error };
type Result = Success | Failure;

function handleResult(result: Result) {
  if (result.status === 'success') {
    console.log(result.data);  // TypeScript knows this is Success
  } else {
    console.error(result.error);  // TypeScript knows this is Failure
  }
}

Exhaustive Checks

function handleStatus(status: Status): string {
  switch (status) {
    case 'pending': return 'Awaiting review';
    case 'approved': return 'Approved';
    case 'rejected': return 'Rejected';
    default:
      const _exhaustive: never = status;  // Error if a case is missing
      throw new Error(`Unhandled status: ${_exhaustive}`);
  }
}

Async Patterns

async/await, Promise.all for concurrency, error handling with try/catch, Promise.allSettled for partial failures, timeout wrappers.

See references/async-patterns.md for full patterns with BAD/GOOD examples.

Common Mistakes

Magic Values

BAD: Hardcoded strings and numbers.

GOOD: Use named constants.

const USER_STATUS = {
  PENDING: 'pending',
  APPROVED: 'approved',
  REJECTED: 'rejected',
} as const;

Type Assertions

BAD: Using as any to bypass type checking.

GOOD: Use @ts-expect-error for known issues. Forces re-evaluation when fixed.

Performance

Prefer for...of over index loops, async APIs over *Sync variants, ?? over || for defaults, destructuring for clarity.

See references/performance-tips.md for the complete set of performance patterns.