Junior 3 min · March 05, 2026

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.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

io/thecodeforge/components/DataTable.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { ReactNode } from 'react';

interface Column<T> {
  header: string;
  accessor: (row: T) => ReactNode;
}

interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
}

export function DataTable<T>({ data, columns }: DataTableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col.header}>{col.header}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map((row, i) => (
          <tr key={i}>
            {columns.map(col => <td key={col.header}>{col.accessor(row)}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing a generic component will help you spot inference issues later.
Production Insight
A generic table that works fine with {id: number, name: string} will break silently if a column tries to access a missing field — but only at runtime.
TypeScript can't check inside the accessor string. Always provide a type‑safe accessor function.
Rule: prefer a function accessor over a string path; it preserves type checking inside the cell renderer.
Key Takeaway
Generics turn components into compile‑time contracts that scale across data shapes.
Fail to constrain them and you trade safety for flexibility — the exact trade you're trying to buy back.
Rule: always constrain your generic with extends to prevent an all‑any escape hatch.
When to use a generic component vs a concrete one
IfComponent accepts multiple data shapes across the app
UseUse a generic component with a constraint extends SomeBaseType
IfComponent is used with only one shape
UseKeep it concrete — generics add unnecessary complexity
IfYou need to restrict which fields can be accessed
UseUse a mapped type like keyof T to enforce valid field names

Discriminated 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'.

io/thecodeforge/hooks/useDataFetch.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type DataState<T> =
  | { status: 'loading' }
  | { status: 'error'; error: string }
  | { status: 'success'; data: T };

function reducer<T>(state: DataState<T>, action: Action<T>): DataState<T> {
  switch (action.type) {
    case 'FETCH_START':
      return { status: 'loading' };
    case 'FETCH_ERROR':
      return { status: 'error', error: action.payload };
    case 'FETCH_SUCCESS':
      return { status: 'success', data: action.payload };
  }
}
Mental Model: State as a Finite Set
  • Loading: no data yet, no error
  • Error: something went wrong, no data
  • Success: data is ready, no error
Production Insight
A developer once used three booleans and a bug set both loading and success. The UI showed a spinner on top of the data — users couldn't click buttons.
A discriminated union would have prevented that state from existing.
Rule: if you have more than two mutually exclusive booleans, switch to a discriminated union.
Key Takeaway
Discriminated unions collapse impossible states into a single type that the compiler enforces.
If a component can only be loading, error, or success, represent it as a union — never as booleans.
Punchline: correct by construction — if it compiles, the state machine is valid.

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.

io/thecodeforge/components/FormInput.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface FormInputProps {
  label: string;
  value: string;
  onChange: (value: string) => void;
}

// RefType, then Props
export const FormInput = React.forwardRef<HTMLInputElement, FormInputProps>(
  ({ label, value, onChange }, ref) => {
    return (
      <label>
        {label}
        <input ref={ref} value={value} onChange={e => onChange(e.target.value)} />
      </label>
    );
  }
);

FormInput.displayName = 'FormInput';
Common Pitfall: Wrong Generic Order
React.forwardRef<Props, RefType>(...) compiles but the ref type will be incorrect. Always put the ref type first: forwardRef<RefType, Props>.
Production Insight
A signup form used the wrong generic order and the ref was typed as unknown. The .focus() call on mount threw a runtime error because unknown has no focus method.
Two hours of tracing led to the generic order — a simple fix with no code change.
Rule: write forwardRef<HTMLInputElement, Props> — ref type first.
Key Takeaway
ForwardRef generics must declare the ref type before the props type.
Wrong order gives you an unknown ref that breaks imperative focus, scroll, and measurement calls.
Remember: ref first, props second — always.

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.

io/thecodeforge/components/MemoizedDataTable.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
// ❌ Bad: React.FC eats the generic
const DataTable: React.FC<DataTableProps<unknown>> = (props) => { ... };

// ✅ Good: keep the generic on the function
function DataTable<T>({ data, columns }: DataTableProps<T>) {
  return <BasicTable data={data} columns={columns} />;
}

export const MemoDataTable = React.memo(DataTable) as typeof DataTable;
Mental Model: React.FC Is a Concrete Wrapper
  • Define generic components as standalone functions, not with React.FC
  • For memoisation, cast the memoised component back to the generic function type
  • Use React.forwardRef similarly without the FC wrapper
Production Insight
A dashboard with 10 generic components compiled in 30 seconds on CI. After removing React.FC wrappers and simplifying generics, compile time dropped to 12 seconds.
The type checker was spending most of its time resolving inferred generics through React.FC.
Rule: avoid React.FC in generic components — define them as bare functions.
Key Takeaway
Generics and React.FC don't mix — the wrapper concretises the type.
React.memo also loses generics unless you cast it back.
Punchline: define generic components as plain functions, not with FC or memo shortcuts.

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.

io/thecodeforge/components/Button.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type ButtonProps = {
  label: string;
  onClick: () => void;
} & (
  | { tooltip?: never; tooltipPosition?: never }
  | { tooltip: string; tooltipPosition: 'top' | 'bottom' }
);

export function Button(props: ButtonProps) {
  if (props.tooltip) {
    // TypeScript narrows: tooltip and tooltipPosition are available
    return <TooltipButton {...props} />;
  }
  return <SimpleButton {...props} />;
}
Forge Insight:
This pattern is sometimes called 'discriminated props'. It avoids the need for if ('tooltip' in props) checks — the union itself guards the branches.
Production Insight
A component with ten conditional props was causing runtime errors because developers missed the documentation. Switching to conditional types turned every wrong usage into a compile error.
The bug surface dropped to zero for that component after the refactor.
Rule: if a prop pair must be used together, model it as a discriminated union in the type — not a comment.
Key Takeaway
Conditional props remove ambiguity by encoding constraints in the type system.
If you write 'tooltipPosition is required only if tooltip is provided', make that a type, not a JSDoc.
Punchline: let the compiler enforce prop relationships — don't trust humans to read comments.
● Production incidentPOST-MORTEMseverity: high

The Missing Generic Annotation That Took Down a Checkout Page

Symptom
Users completed payment details, clicked 'Place Order', and saw a blank white screen. No console errors. No API call logged.
Assumption
The payment form component inferred the correct prop types from the provided data. "It worked in Storybook," said the developer.
Root cause
The generic component 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.
Fix
Explicitly annotate the generic call site: <PaymentForm<CreditCard>> — or redesign the prop to use a discriminated union that forces TypeScript to narrow correctly.
Key lesson
  • 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.
Production debug guideSymptom → Action guide for the three most common type failures in production React apps4 entries
Symptom · 01
Component doesn't accept the props you're passing (TS error in editor) but compiles locally
Fix
Check the tsconfig.json strict flag and ensure skipLibCheck is not masking errors. Run tsc --noEmit to verify build-time types match runtime.
Symptom · 02
Generic component infers an any type for a prop
Fix
Inspect the generic constraint: is it too loose? Add an explicit type annotation at the call site or use a default generic like <T extends Record<string, unknown>>.
Symptom · 03
Ref forwarding produces a TS error on the forwarded component
Fix
Verify that the forwardRef generic is correctly ordered: React.forwardRef<HTMLDivElement, Props> and that the component function accepts ref as the second parameter.
Symptom · 04
Discriminated union state leads to unreachable code in one branch
Fix
Check that the discriminant property (e.g., status) is a literal type, not a generic string. Use a mapped type to exhaustively cover all variants.
★ TypeScript + React Quick Debug Cheat SheetUse these commands and fixes when type inference goes wrong in production
Generic component infers `unknown` instead of expected type
Immediate action
Annotate the generic: `<MyComponent<ConcreteType>`
Commands
tsc --noEmit --strict
console.log type of prop using `typeof` in a test
Fix now
Add a default generic parameter: function MyComponent<T = SomeConcreteType>(props: T)
React.forwardRef type mismatch+
Immediate action
Check the order of generics: forwardRef<RefType, Props>(props, ref)
Commands
tsc --noEmit --strict
Inspect the ref type with `React.RefObject<HTMLDivElement>`
Fix now
Use useImperativeHandle with a typed interface to avoid prop drilling
Discriminated union doesn't narrow in switch/case+
Immediate action
Add a default case with `never` to trigger compile-time error
Commands
tsc --noEmit --strict
Check if discriminant property is optional or nullable
Fix now
Add an exhaustiveCheck(param: never): never helper function
Advanced Patterns Comparison
PatternBest Use CaseRuntime SafetyCompile-Time Overhead
Generic Data ComponentTables, lists, forms with dynamic schemasHigh – type errors caught in editorLow – simple constraint inference
Discriminated Union StateState machines with 2–5 mutually exclusive statesVery High – impossible states preventedLow – union resolution is fast
ForwardRef + GenericsImperative DOM access (focus, scroll)Medium – ref type must be correctLow – only impacts that component
Conditional Props (Union)Components with conditional optional pairs (tooltip + position)Very High – wrong combos become type errorsMedium – complex unions may slow inference
React.FC + Generics (avoid)Don't use this combinationLow – generic lost, runtime errors possibleHigh – slows down type checking for all consumers

Key takeaways

1
Generic components enforce compile‑time contracts across data shapes
always constrain with extends.
2
Discriminated unions eliminate impossible states from your UI state machine.
3
React.forwardRef generics require ref type first, props second
wrong order gives unknown ref.
4
Avoid React.FC for generic components; use plain generic functions instead.
5
Conditional props encode relationships in the type system
let the compiler enforce them.

Common mistakes to avoid

5 patterns
×

Using `React.FC<Props>` with a generic component

Symptom
The generic type parameter is lost. The component can only be used with the concrete type that was specified (often any or unknown).
Fix
Define the component as a generic function directly: function MyComponent<T>(props: Props<T>) instead of const MyComponent: React.FC<Props<T>>.
×

Not constraining a generic parameter

Symptom
TypeScript infers T as any or a too‑wide type. The component accepts any prop shape, defeating the purpose of generics.
Fix
Add a constraint: 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

Symptom
The ref is typed as unknown or the wrong element type. Methods like .focus() cause runtime errors.
Fix
Always write 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

Symptom
Two booleans can be true simultaneously (e.g., loading and success). The UI shows inconsistent states.
Fix
Replace booleans with a discriminated union: { status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: T }.
×

Overcomplicating generics with too many type parameters

Symptom
Compile times increase drastically. Autocomplete becomes sluggish. Other developers struggle to use the component.
Fix
Stick to a single generic parameter for the main data type. Use helper types like Column<T> to derive additional types. Avoid more than 2 type parameters.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you type a generic table component that accepts a dynamic arra...
Q02SENIOR
Explain how discriminated unions improve state management in React compo...
Q03JUNIOR
What is the correct order of generics in React.forwardRef, and what happ...
Q01 of 03SENIOR

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?

ANSWER
Define the component as 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.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the biggest mistake beginners make with TypeScript and React generics?
02
How do I debug a generic component that won't accept the props I pass?
03
When should I use a discriminated union instead of boolean flags for component state?
04
Can I use React.forwardRef with a generic component?
🔥

That's TypeScript. Mark it forged?

3 min read · try the examples if you haven't

Previous
TypeScript Generics
5 / 15 · TypeScript
Next
TypeScript Enums and Decorators