Skip to content
C Codeloom
TypeScript

TypeScript Generics with React

A practical guide to using TypeScript generics in React components, hooks, and props for safer, more reusable building blocks.

·4 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • Generic components
  • Generic hooks
  • Constraints with extends
  • Default type params
  • Inference tips

Prerequisites

  • Comfortable with JS

What and Why

Generics let you write code that works with many types while preserving the exact type at each call site. In React, this is the difference between a List<T> that infers T from your data and a List that forces any. With generics, the renderItem callback knows whether it is rendering a User or a Product, and your IDE autocompletes properties accordingly.

The payoff is real: fewer casts, fewer bugs at boundaries, and components that scale to new data shapes without rewrites.

Mental Model

A generic is a placeholder for a type that gets filled in later, like a function parameter but at the type level. When you write function identity<T>(x: T): T, the compiler chooses T based on the argument. In React, the same idea applies to components and hooks.

  <List<User> items={users} renderItem={u => u.name} />
      |
      v
T = User
      |
      +--> items: User[]
      +--> renderItem: (item: User) => ReactNode
A generic List component flows the item type through props and callbacks

Hands-on Example

A reusable list component:

type ListProps<T> = {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyOf: (item: T) => string;
};

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

Usage infers T automatically:

<List
  items={users}
  keyOf={(u) => u.id}
  renderItem={(u) => <span>{u.name}</span>}
/>

Generic hooks follow the same pattern. A typed local storage hook:

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const raw = localStorage.getItem(key);
    return raw ? (JSON.parse(raw) as T) : initial;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue] as const;
}

Use constraints when T must have a shape:

function pickId<T extends { id: string }>(rows: T[]) {
  return rows.map((r) => r.id);
}

Common Pitfalls

A handful of mistakes recur:

  • Arrow components and JSX ambiguity. const C = <T>(p) => ... looks like JSX to the parser. Use <T,> or <T extends unknown> in .tsx files.
  • Over-constraining. Adding extends object everywhere blocks valid inputs. Constrain only what you actually use.
  • Casting around inference failure. If TypeScript cannot infer T, pass it explicitly: useLocalStorage<User>('u', u). Do not reach for as any.
  • Discarded generics. A type parameter that appears once in the signature provides no benefit. Either use it twice (input and output), or drop it.
  • Wide returns. Returning a tuple? Use as const so consumers see literal positions, not Array<T | Setter>.

Best Practices

Start concrete, then generalize. Write the component for one type first; once a second use case appears, extract a type parameter. Name parameters meaningfully (TItem, TData) when you have more than one. Provide defaults for ergonomic call sites: function useFetch<T = unknown>(url: string) lets users skip the type when they do not care.

Prefer inference over explicit type arguments. If you find yourself writing <User> everywhere, your callback signatures may be hiding the type from the compiler. Make sure prop types reference T directly so inference flows.

Document non-obvious constraints with comments. A type parameter that exists to enforce a relationship between two props (like keyof T in a column prop) deserves a one-line note.

Wrap-up

Generics turn React components into reusable, type-safe primitives. Master a few patterns (generic list, generic hook, constrained parameter), watch out for the JSX arrow gotcha, and you can build libraries that feel like they were written for each consumer.