DEV Community

JSGuruJobs
JSGuruJobs

Posted on

TypeScript Patterns Senior React Engineers Actually Use in 2026

Most React developers use TypeScript for props and useState and stop there.

Senior engineers use TypeScript as an architectural constraint system.

The difference is not syntax knowledge. It is modeling invariants at the type level.

This article focuses on the patterns that eliminate entire bug classes in production React codebases.


1. Discriminated Unions: Model State Machines, Not Optional Props

If your component has many optional props, you likely encoded invalid states.

Instead of:

type Props = {
  variant?: "success" | "error"
  message: string
  onRetry?: () => void
  action?: { label: string; onClick: () => void }
}
Enter fullscreen mode Exit fullscreen mode

Model mutually exclusive states:

type NotificationProps =
  | { variant: "success"; message: string; action?: { label: string; onClick: () => void } }
  | { variant: "error"; message: string; onRetry: () => void }
Enter fullscreen mode Exit fullscreen mode

Now:

  • variant: "error" requires onRetry
  • action cannot exist in the error branch
  • Impossible combinations are compile errors

You are not typing props. You are encoding a state machine.


2. Generics for Reusable Infrastructure

Reusable components without generics are usually unsafe, duplicated per data shape, or loosely typed.

Example: a fully type safe table.

type TableProps<T> = {
  data: T[]
  columns: {
    key: keyof T
    header: string
  }[]
  onRowClick?: (row: T) => void
}

function Table<T>({ data, columns, onRowClick }: TableProps<T>) {
  return null
}
Enter fullscreen mode Exit fullscreen mode

Usage:

<Table
  data={users}
  columns={[
    { key: "name", header: "Name" },
    { key: "email", header: "Email" },
    { key: "doesNotExist", header: "Oops" } // Compile error
  ]}
/>
Enter fullscreen mode Exit fullscreen mode

Generics make infrastructure components aware of domain models without coupling them to specific types.


3. Constrained Generics for Structural Guarantees

Sometimes generics are too permissive.

Constrain them:

type ListProps<T extends { id: string | number }> = {
  items: T[]
  renderItem: (item: T) => React.ReactNode
}
Enter fullscreen mode Exit fullscreen mode

Now all list items must have id. React key safety is enforced at compile time.

You move runtime warnings into compile time guarantees.


4. Polymorphic Components With Proper Prop Inference

Design systems need semantic flexibility:

<Button as="a" href="/pricing" />
<Button onClick={handleClick} />
Enter fullscreen mode Exit fullscreen mode

Type safe polymorphism:

type PolymorphicProps<E extends React.ElementType> = {
  as?: E
} & Omit<React.ComponentPropsWithoutRef<E>, "as">

function Button<E extends React.ElementType = "button">(
  props: PolymorphicProps<E>
) {
  const { as, ...rest } = props
  const Component = as || "button"
  return <Component {...rest} />
}
Enter fullscreen mode Exit fullscreen mode

When as="a", href becomes available.
When omitted, it behaves like a button with correct event types.

This is foundational for serious component libraries.


5. Template Literal Types for Structured APIs

TypeScript can encode structured strings:

type Page = "home" | "product"
type Action = "view" | "click"

type AnalyticsEvent = `${Page}_${Action}`
Enter fullscreen mode Exit fullscreen mode

Now:

track("home_view") // OK
track("home_hover") // Compile error
Enter fullscreen mode Exit fullscreen mode

You eliminate typo driven data corruption.

Template literal types are powerful for analytics events, design tokens, and structured configuration systems.


6. Safe Context Pattern

Raw createContext introduces null into your tree.

Instead of unsafe assertions:

const ThemeContext = React.createContext<Theme | null>(null)
Enter fullscreen mode Exit fullscreen mode

Create a narrowing hook:

function useTheme(): Theme {
  const ctx = React.useContext(ThemeContext)
  if (!ctx) throw new Error("useTheme must be used within ThemeProvider")
  return ctx
}
Enter fullscreen mode Exit fullscreen mode

Consumers now receive a non nullable type.

You remove an entire category of undefined access bugs.


7. satisfies for Configuration Integrity

satisfies validates without widening types:

type RouteConfig = {
  path: string
  auth?: boolean
}

const routes = {
  home: { path: "/" },
  dashboard: { path: "/dashboard", auth: true }
} satisfies Record<string, RouteConfig>
Enter fullscreen mode Exit fullscreen mode

You get validation and preserved literal types at the same time.

Perfect for route maps, feature flags, and design tokens.


8. Runtime Validation at API Boundaries

TypeScript does not validate runtime data.

Use schema driven typing:

import { z } from "zod"

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email()
})

type User = z.infer<typeof UserSchema>

async function fetchUser(id: string): Promise<User> {
  const data = await fetch(`/api/users/${id}`).then(r => r.json())
  return UserSchema.parse(data)
}
Enter fullscreen mode Exit fullscreen mode

The schema becomes both runtime validator and type source of truth.

External data enters your system safely.


What Changes at Senior Level

Junior TypeScript usage:

  • Add interfaces
  • Silence errors
  • Use as when stuck

Senior TypeScript usage:

  • Encode invariants
  • Model state transitions
  • Constrain generic boundaries
  • Remove invalid states entirely

The goal is not stronger typing.

The goal is removing entire bug classes before they exist.

When your type system encodes architectural constraints, React becomes predictable at scale.

Top comments (0)