Back to Blog

Advanced TypeScript Patterns for React

Explore advanced TypeScript patterns that improve type safety, developer experience, and code maintainability in React applications

4 min read
TypeScriptReactType SafetyBest Practices

TypeScript has become essential for building scalable React applications. Let's explore some advanced patterns that go beyond basic typing.

Discriminated Unions for Component Props

One of the most powerful patterns for React components is using discriminated unions to create type-safe prop variants.

typescript
type ButtonProps =
  | {
      variant: 'primary'
      onClick: () => void
      href?: never
    }
  | {
      variant: 'link'
      href: string
      onClick?: never
    }

function Button(props: ButtonProps) {
  if (props.variant === 'primary') {
    // TypeScript knows onClick exists here
    return <button onClick={props.onClick}>Click me</button>
  }

  // TypeScript knows href exists here
  return <a href={props.href}>Link</a>
}

// ✅ Valid
<Button variant="primary" onClick={() => {}} />
<Button variant="link" href="/about" />

// ❌ TypeScript error - can't mix props
<Button variant="primary" href="/about" />

Generic Components

Generic components provide flexibility while maintaining type safety.

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

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

// Usage with full type inference
type User = { id: number; name: string };

<Select
  options={users}
  value={selectedUser}
  onChange={setSelectedUser}
  getLabel={(user) => user.name} // TypeScript infers 'user' is User
  getValue={(user) => user.id.toString()}
/>;

Utility Types for Props

Create reusable utility types to enhance component prop definitions.

typescript
// Make certain props required
type RequireProps<T, K extends keyof T> = T & Required<Pick<T, K>>;

interface FormProps {
  onSubmit?: (data: FormData) => void;
  onError?: (error: Error) => void;
  onSuccess?: () => void;
}

// Require onSubmit
type RequiredFormProps = RequireProps<FormProps, "onSubmit">;

// Exclude props
type Modify<T, R> = Omit<T, keyof R> & R;

interface ButtonBaseProps {
  variant: "primary" | "secondary";
  size: "small" | "medium" | "large";
  disabled?: boolean;
}

// Override the variant prop
type IconButtonProps = Modify<
  ButtonBaseProps,
  {
    variant: "icon";
    icon: React.ReactNode;
  }
>;

Conditional Types for Component Variants

Use conditional types to create intelligent component APIs.

typescript
type InputProps<T extends 'text' | 'number'> = {
  type: T
  value: T extends 'number' ? number : string
  onChange: (value: T extends 'number' ? number : string) => void
}

function Input<T extends 'text' | 'number'>(props: InputProps<T>) {
  return (
    <input
      type={props.type}
      value={props.value}
      onChange={(e) => {
        const val =
          props.type === 'number'
            ? Number(e.target.value)
            : e.target.value
        props.onChange(val as any) // Type assertion needed here
      }}
    />
  )
}

// ✅ Type-safe usage
<Input type="number" value={42} onChange={(val: number) => {}} />
<Input type="text" value="hello" onChange={(val: string) => {}} />

// ❌ TypeScript errors
<Input type="number" value="hello" onChange={(val: number) => {}} />

Type-Safe Event Handlers

Create type-safe event handler utilities.

typescript
type EventHandler<T = Element, E = Event> = (
  event: E & { currentTarget: T }
) => void;

type ChangeHandler<T = HTMLInputElement> = EventHandler<
  T,
  React.ChangeEvent<T>
>;

type ClickHandler<T = HTMLButtonElement> = EventHandler<T, React.MouseEvent<T>>;

// Usage
const handleChange: ChangeHandler = (event) => {
  // event.currentTarget is fully typed as HTMLInputElement
  console.log(event.currentTarget.value);
};

const handleClick: ClickHandler<HTMLAnchorElement> = (event) => {
  // event.currentTarget is typed as HTMLAnchorElement
  console.log(event.currentTarget.href);
};

Strict Null Checks with Optional Chaining

Combine TypeScript's strict null checks with optional chaining for safer code.

typescript
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

function UserCity({ user }: { user: User }) {
  // ✅ Safe access with optional chaining
  const city = user.profile?.address?.city ?? "Unknown";

  // ❌ Without strict null checks, this could crash
  // const city = user.profile.address.city

  return <div>City: {city}</div>;
}

Conclusion

These advanced TypeScript patterns help build more maintainable and type-safe React applications. While they may seem complex at first, they become invaluable as your codebase grows.

The key is finding the right balance—use advanced types where they add value, but don't over-engineer simple components. TypeScript should enhance your development experience, not hinder it.