Home›Blog›JavaScript
JavaScriptTypeScriptReactPatternsBest PracticesFrontend

10 TypeScript Patterns That Make Your React Code Dramatically Better

L
LogicsArcade Team
·Friday, October 9, 2026·4 min read
10 TypeScript Patterns That Make Your React Code Dramatically Better

After years of TypeScript in production React apps, these are the patterns we reach for constantly — from discriminated unions to template literal types.

Why TypeScript Patterns Matter

TypeScript is not just about adding type annotations. Used well, it catches entire categories of bugs at compile time, makes refactoring safe, and serves as documentation that is always up to date. These patterns have saved us from real production bugs.

1. Discriminated Unions for State

Instead of multiple boolean flags that can be in impossible states, use a discriminated union:

// ❌ Allows impossible states
type State = {
  isLoading: boolean;
  data: User | null;
  error: string | null;
};

// ✅ Only valid states are representable
type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; message: string };

2. Template Literal Types

type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

type CSSVariable = `--${string}`;
const color: CSSVariable = "--primary-color"; // valid
const bad: CSSVariable = "primary-color"; // TypeScript error

3. Exhaustive Switch with never

function getStatusLabel(status: "pending" | "active" | "cancelled"): string {
  switch (status) {
    case "pending": return "Waiting";
    case "active": return "Active";
    case "cancelled": return "Cancelled";
    default:
      const _exhaustive: never = status; // compile error if case is missing
      throw new Error(`Unhandled status: ${_exhaustive}`);
  }
}

4. Generic Component Props

interface SelectProps<T> {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  getLabel: (item: T) => string;
  getValue: (item: T) => string;
}

function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select value={getValue(value)} onChange={e => {
      const selected = options.find(o => getValue(o) === e.target.value);
      if (selected) onChange(selected);
    }}>
      {options.map(opt => (
        <option key={getValue(opt)} value={getValue(opt)}>{getLabel(opt)}</option>
      ))}
    </select>
  );
}

5. Const Assertions for Config Objects

const ROUTES = {
  home: "/",
  blog: "/blog",
  admin: "/admin",
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];
// "/" | "/blog" | "/admin"
// Adding a new route makes it automatically available in the type

6. Mapped Types for Form Errors

interface SignupForm {
  email: string;
  password: string;
  name: string;
}

type FormErrors = Partial<Record<keyof SignupForm, string>>;
// { email?: string; password?: string; name?: string }
// Automatically stays in sync with the form interface

7. Branded Types for IDs

type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };

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

function getUser(id: UserId): User { ... }

const postId = createPostId("post_123");
getUser(postId); // TypeScript error — wrong brand!

8. Conditional Types for API Responses

type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  try {
    const user = await db.users.findById(id);
    return { success: true, data: user };
  } catch (e) {
    return { success: false, error: "User not found" };
  }
}

// Usage — TypeScript enforces the check
const result = await fetchUser("123");
if (result.success) {
  console.log(result.data.name); // type-safe access
} else {
  console.error(result.error);
}

9. Strict Event Handler Typing

// Instead of any in event handlers
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  setValue(e.target.value);
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  // ...
}

// For custom events with data
type CustomEvent<T> = { detail: T };
function handleCustom(e: CustomEvent<{ userId: string }>) {
  console.log(e.detail.userId);
}

10. Utility Type Combinations

interface User {
  id: string;
  email: string;
  name: string;
  role: "admin" | "user";
  createdAt: Date;
}

// Form for creating a user (no id or createdAt, role defaults)
type CreateUserInput = Omit<User, "id" | "createdAt" | "role"> & {
  role?: User["role"];
};

// For updating — all fields optional except id
type UpdateUserInput = Required<Pick<User, "id">> & Partial<Omit<User, "id">>;

These patterns are not just syntactic tricks — they encode domain rules into the type system, turning potential runtime bugs into compile-time errors that you catch before they reach production.

← Back to Blog