Advanced React + TS Patterns

Hard

Advanced React TypeScript patterns combine generics, conditional types, and mapped types to create type-safe APIs for complex component libraries. These patterns include generic list/table components, polymorphic `as` props, strictly typed context with no undefined checks, and form handling with inferred field types. Mastering these patterns is what separates senior frontend engineers from intermediate ones.

Interactive Visualization

Key Points

  • Polymorphic `as` prop lets a component render as any HTML element or component
  • Generic components maintain type relationships between data and render callbacks
  • Strictly typed context factories eliminate undefined checks at usage sites
  • Compound components use React context with TypeScript for type-safe slot patterns
  • Forwarded refs require `React.forwardRef` with explicit generic type parameters
  • Template literal types can generate prop types from string patterns

Code Examples

Polymorphic Components

type PolymorphicProps<E extends React.ElementType, P = object> = P &
  Omit<React.ComponentPropsWithoutRef<E>, keyof P | 'as'> & {
    as?: E
  }

// Box component that can render as any element
function Box<E extends React.ElementType = 'div'>({
  as,
  children,
  ...rest
}: PolymorphicProps<E, { children?: React.ReactNode }>) {
  const Component = as || 'div'
  return <Component {...rest}>{children}</Component>
}

// Usage
<Box as="section" id="main">Content</Box>
<Box as="a" href="/home">Link</Box>
// <Box as="a" href={42} />  // Error: href must be string

// With custom props
type TextProps<E extends React.ElementType> = PolymorphicProps<E, {
  size?: 'sm' | 'md' | 'lg'
  weight?: 'normal' | 'bold'
}>

function Text<E extends React.ElementType = 'span'>({
  as, size, weight, ...rest
}: TextProps<E>) {
  const Component = as || 'span'
  return <Component {...rest} />
}

Polymorphic components accept an `as` prop that changes the rendered element. TypeScript ensures that the remaining props match the element type, preventing invalid prop combinations.

Strictly Typed Context

// Context factory — eliminates undefined checks at usage sites
function createStrictContext<T>(displayName: string): [
  React.Provider<T>,
  () => T,
] {
  const Context = React.createContext<T | undefined>(undefined)
  Context.displayName = displayName

  function useStrictContext(): T {
    const value = React.useContext(Context)
    if (value === undefined) {
      throw new Error(
        `use${displayName} must be used within a ${displayName}Provider`
      )
    }
    return value
  }

  return [Context.Provider as React.Provider<T>, useStrictContext]
}

// Usage
interface AuthContext {
  user: User
  logout: () => void
}

const [AuthProvider, useAuth] = createStrictContext<AuthContext>('Auth')

// In component — no undefined check needed
function Profile() {
  const { user, logout } = useAuth()  // AuthContext, not AuthContext | undefined
  return <div>{user.name}</div>
}

interface User { name: string; email: string }

The strict context factory pattern creates a context and hook pair where the hook throws if used outside the provider. This eliminates the need for undefined checks at every usage site.

Generic List Component

// Generic list with type-safe item rendering
interface ListProps<T> {
  items: T[]
  keyExtractor: (item: T) => string
  renderItem: (item: T, index: number) => React.ReactNode
  emptyMessage?: string
}

function List<T>({
  items,
  keyExtractor,
  renderItem,
  emptyMessage = 'No items',
}: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty">{emptyMessage}</div>
  }

  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  )
}

// Usage — T is inferred from items
interface Todo {
  id: string
  text: string
  done: boolean
}

const todos: Todo[] = []

<List
  items={todos}
  keyExtractor={t => t.id}
  renderItem={(todo) => (
    <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
      {todo.text}
    </span>
  )}
/>

Generic list components infer their type parameter from the items array. The renderItem callback then receives properly typed items, enabling type-safe rendering without manual type annotations.

Common Mistakes

  • Making polymorphic components too complex — not all components need the `as` pattern
  • Not using context factories, leading to `| undefined` checks everywhere
  • Forgetting to handle the empty/default case in generic components
  • Over-abstracting with generics when a simpler component with specific props would suffice
  • Not testing polymorphic components with various element types to catch type issues

Interview Tips

  • The polymorphic `as` pattern shows deep TypeScript + React knowledge
  • Explain the strict context factory — it solves a real pain point in React + TS
  • Generic components demonstrate both React architecture and TypeScript fluency
  • Know when these patterns are worth the complexity vs simpler alternatives

Related Concepts