Siddhant DevalAuthor
Senior Full-Stack Engineer·Aug 27, 2026·14 min read
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.
Technical Series
TypeScript Mastery
Part 11 of 11
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:typescript
Architectural Note
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:typescript
Performance / Safety Warning
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'.](/assets/blog/frontend/typescript-react-components-hooks/fig-01.png)
Expand
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:
typescript
3.2 ComponentPropsWithRef vs ComponentPropsWithoutRef
typescript
4. forwardRef with Generics
forwardRef requires explicit typing for the ref and props:typescript

Expand
5. Typing Event Handlers
typescript
6. useRef — Two Distinct Modes
useRef serves two distinct purposes, and each requires a different initial value:typescript
Crucial Requirement
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:typescript

Expand
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:typescript
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 |
Research & Synthesis Note
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.
#TypeScript#React#Components#Hooks#Generic Components