TypeScript with React: Components, Hooks & Patterns
React's generic component model and hook return types are the most common TypeScript surface area for frontend engineers — yet most teams reach for `any` or `as` unnecessarily. Every pattern here eliminates one of those shortcuts with a typed, composable alternative.
TypeScript Mastery
TypeScript with React: Components, Hooks & Patterns
Types are a specification language — not an annotation layer. In React codebases, that principle is violated most often with component props: either any appears in a generic component because the developer didn't know how to thread the type parameter, or as appears in a hook to "fix" a narrowing issue that a correctly typed signature would prevent entirely. This article removes both shortcuts by building every common React typing pattern from first principles.
1. Functional Components — FC<Props> vs. Plain Function
The older React.FC<Props> pattern adds an implicit children prop to every component, whether or not it accepts children. This is almost always wrong:
In React 18, FC no longer includes an implicit children prop. The plain function pattern is still preferred for its explicitness — the Props interface is the canonical place for all prop declarations.
2. Generic Components
Generic components are where most developers reach for any. The correct tool is a type parameter on the function:
In .tsx files, the TypeScript parser may confuse <T> at the start of a generic component with a JSX element. Use the <T,> trailing comma or <T extends unknown> syntax to disambiguate: function Component<T extends unknown>({ ... }).
![Three-box flow diagram. Left box: items: User[] with T = User highlighted. Center box: generic function List<T> with the arrow labeled 'T = User inferred from items'. Right box: renderItem: (item: User) => ReactNode — item is typed as User, not any. A second row below shows the same diagram with items: Product[] → T = Product → renderItem: (item: Product) => ReactNode. Caption: 'The generic type parameter T flows from the items array element type to the renderItem callback argument — both are typed without any annotations on the call site'.](https://pub-74778554195b4df89d82f0d61988355a.r2.dev/assets/blog/frontend/typescript-react-components-hooks/fig-01.png)
3. Extending Component Props
3.1 ComponentProps<typeof Component>
When wrapping a component, extend its props exactly — so changes to the wrapped component's API automatically propagate:
3.2 ComponentPropsWithRef vs ComponentPropsWithoutRef
4. forwardRef with Generics
forwardRef requires explicit typing for the ref and props:

5. Typing Event Handlers
6. useRef — Two Distinct Modes
useRef serves two distinct purposes, and each requires a different initial value:
Passing null to useRef<HTMLInputElement>(null) makes .current readonly (a RefObject) — this is what ref={...} on JSX elements expects. Passing a value (e.g., useRef<number>(0)) makes .current writable (a MutableRefObject). Mixing these up is a common source of TypeScript errors.
7. Typed useReducer with Discriminated Union Actions
useReducer with a discriminated union action type gives you exhaustive dispatch — the compiler catches missing cases when a new action is added:

8. Context Typing — The Narrowing Custom Hook Pattern
createContext with null as the default combined with a narrowing hook eliminates the optional chaining boilerplate at every call site:
Summary
| Concept | Rule |
|---|---|
FC<Props> |
Avoid — adds implicit children prop; use plain function with explicit interface |
| Generic component | function List<T>({ items }: { items: T[] }) — T flows from items to renderItem |
ComponentProps<typeof X> |
Correct way to extend a component's props in a wrapper — tracks changes automatically |
forwardRef<RefType, PropsType> |
Both type params required — RefType is what the parent's useRef sees |
React.ChangeEvent<HTMLInputElement> |
Use React's typed events — not the browser's Event which loses the element type |
useRef<T>(null) |
DOM ref — .current is T | null, readonly (RefObject) |
useRef<T>(value) |
Mutable container — .current is T, writable (MutableRefObject) |
| Discriminated union actions | useReducer + union actions → exhaustive dispatch; compiler catches missing cases |
createContext<T | null>(null) |
Use null default + narrowing hook — consumers get non-null type without casting |
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.