skip to content

Generic Component & Hook Props

How a reusable list, table, or select component stays typed for whatever item type the caller passes, so renderItem receives the right element type. A standard frontend-TypeScript interview exercise, plus the .tsx syntax gotcha that comes with it.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In a .tsx file, how do you type the props of a reusable List component so that its renderItem callback receives the element type of the items array instead of any, and where does that type come from at each usage?

level: middleimportance: must knowfreq 62%

answer

  1. parameter belongs on the component function
  2. props type is a family, not a type
  3. one prop supplies the type argument
  4. the callback is contextually typed, not inferred from
  5. empty array collapses T to never

basics

~20 s

Declare the type parameter on the component function and thread it through the props: function List<T>(props: { items: T[]; renderItem: (item: T) => ReactNode }). TypeScript infers T from the items passed at each usage site.

solid answer

~50 s

The pattern is a generic props type plus a generic component function. Declare `interface ListProps<T> { items: readonly T[]; renderItem: (item: T, index: number) => ReactNode }` and then `function List<T>(props: ListProps<T>)`. The type parameter has to live on the **function**, because that is what gets instantiated per usage — a generic interface alone is just a family of types with nothing binding `T`. At each call site TypeScript infers `T` from the `items` argument, and `renderItem` is then contextually typed, so the callback parameter is the element type without any annotation from the caller. If items must carry a key you constrain it, `function List<T extends { id: string }>(…)`, which is checked at the usage site while `renderItem` still sees the full `T`. All of this is erased at compile time: the emitted component is an ordinary function with no knowledge of `T`.

code

typescript · 12 lines
typescript
interface ListProps<T> {
  items: readonly T[];
  renderItem: (item: T, index: number) => string;
}

function renderList<T>({ items, renderItem }: ListProps<T>): string {
  return items.map(renderItem).join("");
}

// T is inferred as number from `items`; `n` is number, not any
const out = renderList({ items: [1, 2, 3], renderItem: (n) => `<li>${n}</li>` });
console.log(out);

go deeper

for a junior

Know that writing function List<T>(props: ListProps<T>) lets one component work with any element type, and that the caller does not have to annotate the render callback.

for a middle

Be ready to explain both declaration sites, say that the items prop is what the type argument is inferred from, and note that the render callback is contextually typed rather than an inference source.

for a senior

Show judgment about how far to constrain the parameter and which props to relate to it — keyof T for a sort key, (row: T) => void for callbacks — and explain what breaks when a usage supplies no inference candidate.

for a principal

Own the API-design tradeoff: a generic component pushes type knowledge to the call site and keeps the library honest, but each added parameter is a public commitment. Argue when a plain union prop is the cheaper contract.

## The problem this pattern solves A reusable list, table, or select renders a collection it knows nothing about. It needs two things from the caller: the collection, and a function that turns one element into output. If the props type pins the element type down — `items: unknown[]` with `renderItem: (item: unknown) => ReactNode` — then every caller has to re-assert the type inside its own callback, and every assertion is an unchecked claim the compiler simply believes. Typing the component generically pushes that knowledge back to the one place that actually has it: the call site. ## The shape ```typescript interface ListProps<T> { items: readonly T[]; renderItem: (item: T, index: number) => string; keyOf?: (item: T) => string; } function renderList<T>({ items, renderItem }: ListProps<T>): string { return items.map(renderItem).join(""); } ``` `ListProps<T>` is a *generic interface*: not a type but a family of types, parameterised by `T`. Writing `ListProps` alone is an error — it must be applied to something. The component function declares its **own** `T` and applies the interface to it. That second declaration is the load-bearing part, and it is where most candidates go wrong: a generic props interface used at one fixed argument (`ListProps<unknown>`) makes the component monomorphic, and every consumer shares that single element type. ## Where T actually comes from TypeScript infers a type argument for `T` from the arguments at the call. In JSX, `<List items={users} renderItem={(u) => u.name} />` gives the checker one strong candidate — the `items` prop, of type `User[]` — so `T` resolves to `User`. The `renderItem` arrow is not an inference *source*; it is **contextually typed** by the already-resolved `ListProps<User>`, which is why `u` comes out as `User` rather than an implicit `any`. That asymmetry is worth being able to state out loud, because it explains the failure cases: - `items={[]}` supplies no candidate, so `T` infers as `never` and `renderItem`'s parameter becomes unusable. - If the caller annotates the callback parameter with a wider type, the mismatch surfaces at the callback, not at `items`. - You can bypass inference entirely by writing the type argument explicitly on the element: `<List<User> items={rows} … />`. ## Putting the parameter in the wrong place Two popular wrong shapes: ```typescript // 1) parameter only on the props type — nothing binds it per usage declare const ListA: (props: ListProps<unknown>) => string; // 2) items typed loosely — every caller casts declare const ListB: (props: { items: any[]; renderItem: (item: any) => string }) => string; ``` The first is what you get if you reach for a pre-typed component-function alias such as React's `FC`: that alias takes the props type as *its* argument, so you must hand it a concrete one; there is nowhere to put a parameter that is re-inferred at every usage. The plain generic function declaration is the idiomatic answer, and in a `.tsx` file an arrow needs the disambiguating trailing comma, `const List = <T,>(props: ListProps<T>) => …`, because bare `<T>` is read as a JSX tag. ## Constraints and relating props to each other Once `T` exists, other props can be expressed in terms of it, which is the real payoff: ```typescript interface TableProps<T> { rows: readonly T[]; sortBy: keyof T; // only real keys of the row type onSelect: (row: T) => void; // callback gets the row type back } ``` Constrain when the component genuinely needs a capability — `function List<T extends { id: string }>` — and not out of habit; an over-tight constraint rejects callers for no benefit. A constraint is also what lets you *use* the value inside the component rather than merely pass it along. ## Nothing survives compilation `T` is a compile-time device only. The emitted component is a plain function; there is no way to branch on `T` at runtime, no `if (T === User)`. If behaviour must vary by kind, the caller passes a real prop — a discriminant string, or the render function itself. Candidates who propose runtime inspection of the type parameter are describing a language TypeScript is not. ## What good answers include Name the two declaration sites (props type and function), name `items` as the inference source, name contextual typing as the reason the callback parameter is typed for free, and mention erasure. That is the complete answer; utility-type gymnastics are not required and usually signal the candidate is avoiding the actual mechanism.

  • Where exactly does the checker get T from at a usage, and what happens if the caller passes an empty array literal?
    From the `items` argument: it is the only prop that supplies a real inference candidate, so `T` is fixed before the render callback is checked. An empty array literal supplies no candidate, so `T` infers as `never` and `renderItem`'s parameter becomes `never` — usually surfacing as a confusing error inside the callback. The fix is an explicit type argument at the usage, or typing the caller's variable.
  • How would you require every item to carry an id, and what changes for the caller?
    Constrain the parameter: `function List<T extends { id: string }>(props: ListProps<T>)`. The constraint is checked where the component is used, so a caller passing objects without `id` gets an error on the `items` prop. Inside the component you may now read `item.id`, and `renderItem` still receives the full `T`, not the narrowed constraint — inference keeps the caller's concrete type.
  • Can the component change behaviour based on what T turned out to be?
    No. Type parameters are erased; the emitted function has no record of `T`. Anything conditional has to travel as an actual value — a discriminant prop, a comparator, or the render function itself. This is the standard follow-up trap: a candidate who proposes checking `T` at runtime has confused the type layer with the emitted JavaScript.

saying these in an interview costs you the question

  • Thinks a generic props interface alone makes the component generic
  • Types items as any[] and casts inside every renderItem
  • Believes the type parameter can be inspected at runtime
  • Says the caller must always annotate the renderItem parameter
  • Reaches for a pre-typed component alias that takes fixed props

context

open as a page

In a .tsx file, `const identity = <T>(value: T) => value;` fails to compile although the identical line is fine in a .ts file. Why, and what are two ways to write it so it parses?

level: juniorimportance: should knowfreq 45%

basics

~20 s

In .tsx files the parser reads the leading angle bracket as the start of a JSX element, not a type parameter list. Disambiguate with a trailing comma, <T,>, add a constraint such as <T extends unknown>, or use function syntax.

open as a page

A generic custom hook ends with `return [value, setValue];` and has no return type annotation. Why does the caller's destructured setValue come out with a union type, and how do you type the hook so the pair destructures correctly?

level: middleimportance: should knowfreq 42%

basics

~20 s

An array literal is inferred as an array of the union of its element types, so both destructured names get value-or-setter. Annotate the return type as a tuple, such as [T, (next: T) => void], or return the literal with as const.

open as a page

You pass a generic component `function List<T>(props: ListProps<T>): string` to a helper declared as `function wrap<P>(component: (props: P) => string): (props: P) => string`. The wrapped result no longer infers the item type at each usage. Which TypeScript rule causes that, and what is the standard fix?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The helper's parameter is an ordinary, non-generic function type, so passing a generic function instantiates its type parameter once — usually to unknown — and the result is a single concrete component. The standard fix is to assert the wrapped value back to the original signature.

open as a page