TypeScript Generics Explained: Real-World Patterns for React Developers | SoniNow Blog

Limited TimeLearn More

typescriptgenericsreacttype safetyweb development

TypeScript Generics Explained: Real-World Patterns for React Developers

Published

2026-06-23

Read Time

4 mins

TypeScript Generics Explained: Real-World Patterns for React Developers

Generics are TypeScript's most powerful feature for writing reusable, type-safe code. Many React developers skip them because the syntax looks intimidating. But once you see generics through the lens of real component and hook patterns, they become indispensable — and surprisingly readable.

Generic Components That Adapt to Their Props

A generic component accepts a type parameter that flows through its props, state, and return values. The classic example is a typed list component:

interface ListProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
  keyExtractor: (item: T) => string
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  )
}

// Usage — TypeScript infers T from the items array
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

TypeScript infers T from the items prop. You get autocomplete for user.name and user.id — no explicit type annotation needed at the call site. This pattern eliminates dozens of nearly identical component variants.

Type-Safe Custom Hooks

Generics shine in custom hooks where input and output types are structurally related. A useFetch hook demonstrates this cleanly:

interface UseFetchResult<T> {
  data: T | null
  isLoading: boolean
  error: Error | null
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    fetch(url)
      .then((res) => res.json() as Promise<T>)
      .then(setData)
      .catch(setError)
      .finally(() => setIsLoading(false))
  }, [url])

  return { data, isLoading, error }
}

// Usage — typed response without casting
const { data } = useFetch<User[]>('/api/users')
// data is typed as User[] | null

The generic parameter flows from the call site through the entire hook. A real production hook would handle cancellation, caching, and race conditions, but the generic foundation is the same.

Utility Types That Reduce Boilerplate

Combine generics with TypeScript's built-in utility types to transform your existing types without manual redefinitions:

// Type-safe state setter that only accepts valid keys
function createStateUpdater<T extends object>(obj: T) {
  return function <K extends keyof T>(key: K, value: T[K]) {
    return { ...obj, [key]: value }
  }
}

type ApiResponse<T> = {
  data: T
  status: number
  message: string
}

// Pick specific fields for a table view
type TableUser = Pick<User, 'id' | 'name' | 'email'>

The K extends keyof T constraint is one of the most useful patterns in React development. It ensures you can only pass valid property keys to updaters, reducers, or column definitions.

Generic Reducers for Complex State

When useReducer manages complex state, generics keep the actions type-safe across large codebases:

type ActionMap<T extends { [key: string]: unknown }> = {
  [K in keyof T]: { type: K; payload: T[K] }
}

type ProductActions = ActionMap<{
  SET_NAME: string
  SET_PRICE: number
  SET_TAGS: string[]
  RESET: undefined
}>

type ProductAction = ProductActions[keyof ProductActions]

This pattern ensures every dispatched action carries the correct payload type. No more runtime crashes because a reducer received a string where it expected a number.

When Generics Go Too Far

Generics add complexity. Do not use them for one-off components or hooks used in a single context. Reserve generics for abstractions that genuinely serve multiple data types — list components, form field wrappers, data-fetching hooks, and reducer action maps. Simple functions should stay simple.

Mastering TypeScript generics transforms how you structure React applications. The result is code that catches type errors at compile time rather than in production. At SoniNow, we write type-safe React applications where generics eliminate entire categories of runtime bugs before they reach your users.

Want to level up your TypeScript architecture? Contact SoniNow for a code review or to discuss how we can bring type safety to your next project.