TypeScript React — Missing Generic Crashed Checkout
Blank white screen after payment? A missing generic annotation caused it — use expect-type to catch this silently failing error, unlike GFG's basic guides.
- Generic components enforce type contracts across data shapes at compile time
- Discriminated unions model finite states (loading/success/error) with zero ambiguity
- React.forwardRef + generics = typed refs without
- TypeScript infers from usage, but complex generics need explicit annotations sometimes
- Common trap: over-constraining generics when simpler overloads work better
Imagine you're building with LEGO. Plain React is like LEGO with no instructions — any piece can technically snap onto any other, but you'll find out it's wrong only when the model collapses. TypeScript is the instruction manual that says 'this blue 2x4 brick only connects to these specific pieces' — before you've even started building. It turns runtime crashes into editor squiggles you fix in seconds, not 3am production incidents.
React gives you freedom. TypeScript gives you guardrails. Together they give you something rare in frontend engineering: confidence that a refactor won't silently break a prop three components deep. In a codebase with a dozen engineers, untyped props are a game of telephone — by the time the wrong shape reaches the component that actually needs it, the original author has left the company. TypeScript closes that gap and makes intent explicit at the component boundary.
The problem TypeScript solves in React isn't just catching typos. It's expressing contracts. A generic data table shouldn't accept 'any data' — it should accept 'an array of T, and a set of column definitions that know how to read properties of T'. Without TypeScript that constraint lives only in a comment nobody reads. With TypeScript it's enforced by the compiler, autocompleted in the editor, and self-documenting in the type signature.
After reading this article you'll be able to type generic components that work across multiple data shapes, model complex UI state with discriminated unions so impossible states become compiler errors, forward refs with full type safety, and avoid the five performance and type-inference traps that catch even experienced React + TypeScript developers off guard. These are patterns straight from production codebases — not toy examples.
What is TypeScript with React?
TypeScript with React lets you bake type safety into component boundaries. Instead of runtime prop‑checking with PropTypes, you encode contracts at compile time. The real win: when a prop shape changes, every consumer lights up with errors — not silent breakage. In practice, that means a generic DataTable<T> can accept any data shape and enforce column definitions that know how to read fields of T. This isn't just about catching null dereferences; it's about making impossible states unrepresentable. For instance, a modal that shows either a loading spinner, an error message, or content should express that as a discriminated union — TypeScript then guarantees you can't accidentally render content while loading.
{id: number, name: string} will break silently if a column tries to access a missing field — but only at runtime.extends to prevent an all‑any escape hatch.extends SomeBaseTypekeyof T to enforce valid field namesDiscriminated Unions for Complex State Management
A state machine inside a React component is common: loading, success, error. The naive approach uses three separate booleans — but that lets you accidentally set both loading and error to true. A discriminated union collapses these into a single type that exactly one branch can hold. Use the status or state property as the discriminant and map over each variant. The payoff: every render path is explicit, and TypeScript will yell if you try to access data when status is 'error'.
- Loading: no data yet, no error
- Error: something went wrong, no data
- Success: data is ready, no error
loading and success. The UI showed a spinner on top of the data — users couldn't click buttons.Forwarding Refs with Type Safety
React.forwardRef lets parent components grab a DOM node from a child. Without generics, the ref type is any and you lose all autocomplete. With a generic forwardRef, you keep the connection between the ref type and the underlying DOM element. The trick: the order of generics is RefType, Props — not Props, RefType. Mix them up and TypeScript will silently infer a wrong type. Common use case: wrapping a third‑party input library where the parent needs to call .focus() imperatively.
forwardRef<RefType, Props>.unknown. The .focus() call on mount threw a runtime error because unknown has no focus method.forwardRef<HTMLInputElement, Props> — ref type first.unknown ref that breaks imperative focus, scroll, and measurement calls.Performance Gotchas: Type Inference Limits and Memoization
TypeScript's type inference is powerful but not free. Deeply nested generics — especially in large union types — can slow down editor autocompletion and increase compilation time. Worse, if a generic component is used inside a React.memo wrapper, the generic type can be lost because memo infers the props as a concrete type. The fix: explicitly annotate the memoised component with the generic signature. Another trap: using React.FC<Props> with a generic component forces you to lose the generic parameter — it's a concrete type wrapper. Instead, define the function directly with the generic.
- Define generic components as standalone functions, not with React.FC
- For memoisation, cast the memoised component back to the generic function type
- Use
React.forwardRefsimilarly without the FC wrapper
React.FC wrappers and simplifying generics, compile time dropped to 12 seconds.React.FC.React.FC in generic components — define them as bare functions.React.FC don't mix — the wrapper concretises the type.React.memo also loses generics unless you cast it back.Conditional Props and Type Narrowing with Generics
Sometimes a component's props change based on another prop value. For example, a Button component that optionally renders a tooltip when tooltip prop is provided, and then requires a tooltipPosition. Conditional types let you encode these rules: if tooltip is string, then tooltipPosition is required; otherwise it's forbidden. This eliminates runtime checks and makes the API self‑documenting. The key is to use a discriminated union on the component's props type itself, not just on state.
if ('tooltip' in props) checks — the union itself guards the branches.The Missing Generic Annotation That Took Down a Checkout Page
PaymentForm<T> received an array of mixed payment methods. TypeScript inferred T as the union of all possible values, not the specific method selected, causing the submit handler to receive an incorrectly typed object — and the render branch that handled the correct shape was never reached.<PaymentForm<CreditCard>> — or redesign the prop to use a discriminated union that forces TypeScript to narrow correctly.- Always specify the generic parameter explicitly when the inferred type might be wider than intended.
- Test generic components with every concrete type they'll receive in production — not just the happy path.
- Add a compile-time assertion (e.g.,
expect-type) to verify the generic resolves to the expected shape.
strict flag and ensure skipLibCheck is not masking errors. Run tsc --noEmit to verify build-time types match runtime.any type for a prop<T extends Record<string, unknown>>.React.forwardRef<HTMLDivElement, Props> and that the component function accepts ref as the second parameter.status) is a literal type, not a generic string. Use a mapped type to exhaustively cover all variants.function MyComponent<T = SomeConcreteType>(props: T)Key takeaways
extends.unknown ref.React.FC for generic components; use plain generic functions instead.Common mistakes to avoid
5 patternsUsing `React.FC<Props>` with a generic component
any or unknown).function MyComponent<T>(props: Props<T>) instead of const MyComponent: React.FC<Props<T>>.Not constraining a generic parameter
T as any or a too‑wide type. The component accepts any prop shape, defeating the purpose of generics.function DataTable<T extends Record<string, unknown>>(props: Props<T>). This ensures T is an object and enables keyof access.Mixing up the generic order in React.forwardRef
unknown or the wrong element type. Methods like .focus() cause runtime errors.React.forwardRef<RefType, Props>(...). The ref type comes first, then the props type.Using three booleans for loading/error/success instead of a discriminated union
{ status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: T }.Overcomplicating generics with too many type parameters
Column<T> to derive additional types. Avoid more than 2 type parameters.Interview Questions on This Topic
How would you type a generic table component that accepts a dynamic array of columns, where each column can render a function of the row data?
function DataTable<T>({ data, columns }: { data: T[]; columns: { header: string; render: (row: T) => ReactNode }[] }). The generic T is inferred from the data prop. Each column's render function receives a typed row. This pattern ensures type safety across all column renderers.Frequently Asked Questions
That's TypeScript. Mark it forged?
3 min read · try the examples if you haven't