Home JavaScript TypeScript React — Missing Generic Crashed Checkout
Advanced 6 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 & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Deep production experience
  • Understanding of internals and trade-offs
  • Experience debugging complex systems
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
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.

Imagine you're building with LEGO.

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.

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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 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>
  );
}
Try it live
🔥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
typescript-with-react THECODEFORGE.IO Type-Safe Checkout with Discriminated Unions Prevent crashes by modeling payment state as a discriminated union Define Union Type type CheckoutState = { status: 'idle' } | { status: 'loading' } | { status: 'suc Use in Reducer Switch on status to narrow type and access relevant fields Render Based on State Conditional rendering with type narrowing ensures no missing cases Handle Missing Generic If generic is omitted, TypeScript defaults to unknown, causing crash Add Generic Parameter Use to enforce type safety ⚠ Missing generic leads to 'data' being unknown Always specify the union type parameter explicitly THECODEFORGE.IO
thecodeforge.io
Typescript With React

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 };
  }
}
Try it live
Mental Model
Mental Model: State as a Finite Set
A component can be in exactly one of these three states — never two at once.
  • 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';
Try it live
⚠ 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.
typescript-with-react THECODEFORGE.IO React Component Hierarchy for Checkout Layered architecture with type-safe refs and conditional props UI Components CheckoutForm | PaymentButton | OrderSummary State Management useReducer with Discriminated | Context Provider Type Safety Layer Generic Props | Conditional Types | Ref Forwarding with Generics Data Layer API Service | Error Handling THECODEFORGE.IO
thecodeforge.io
Typescript With React

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 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;
Try it live
Mental Model
Mental Model: React.FC Is a Concrete Wrapper
React.FC<Props> is shorthand for a function that takes Props and returns JSX — it cannot carry a generic parameter.
  • 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} />;
}
Try it live
🔥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.

Stop Using `any` — A Type-Safe State Management Pattern

Junior devs love any. It feels faster. It is not. I just spent three hours on-call debugging a production crash because someone used any on a user object that changed shape between API versions. The type system is your first line of defense, not your enemy.

Here is the real reason any is dangerous: it disables all type checking downstream. One any pollutes your entire component tree. The fix is a discriminated union for your API response. Model every possible state — loading, success, error, empty. Then narrow with type guards. If your API returns a new field you didn't expect, TypeScript screams at compile time, not in production at 2 AM.

This pattern forces you to handle every state explicitly. No more if (data) { ... } and hoping. The match function ensures you return something for every branch. Your future self will thank you when the PM asks for a loading spinner.

useApiState.tsTYPESCRIPT
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
// io.thecodeforge
type ApiState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }
  | { status: 'empty' };

function match<T, R>(
  state: ApiState<T>,
  handlers: {
    loading: () => R;
    success: (data: T) => R;
    error: (err: Error) => R;
    empty: () => R;
  }
): R {
  switch (state.status) {
    case 'loading': return handlers.loading();
    case 'success': return handlers.success(state.data);
    case 'error': return handlers.error(state.error);
    case 'empty': return handlers.empty();
  }
}

export { ApiState, match };
Output
Compile-time safety: if you miss a case, TypeScript errors at build time.
Try it live
⚠ Production Trap:
We had a user profile component that assumed data.email existed. After an API refactor, email became optional. Because we used any, no one caught it. Users got blank profile pages for three days before someone noticed the logs.
🎯 Key Takeaway
Never use any for API data. Model every state explicitly with a discriminated union.

Why Your UseRef Is Breaking TypeScript — And How to Fix It

I've seen more production fires from useRef misuse than props. The pattern is always the same: a junior dev does const ref = useRef<HTMLInputElement>(null) and then tries to access ref.current.value without checking. TypeScript doesn't save them because current can be null. So they add a non-null assertion (!) and the bug disappears... until the component unmounts early.

Here is the fix: useRef with null as initial value means the ref is MutableRefObject<HTMLInputElement | null>. You must guard every access. But there is a smarter pattern — use a callback ref. It gives you the DOM node synchronously.

The callback ref pattern is safer because TypeScript narrows the type automatically inside the callback. No ?. operators everywhere. No runtime crashes from stale refs. This matters most in event handlers that fire after a component unmounts (like async data calls). Use this for form inputs, canvas elements, or any DOM node you manipulate directly.

SafeTextInput.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
// io.thecodeforge
import { useState, useCallback } from 'react';

interface SafeTextInputProps {
  onValue: (value: string) => void;
}

const SafeTextInput: React.FC<SafeTextInputProps> = ({ onValue }) => {
  const [text, setText] = useState('');
  
  const inputRef = useCallback((node: HTMLInputElement | null) => {
    if (node !== null) {
      // node is definitely HTMLInputElement here
      node.focus();
    }
  }, []);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
    onValue(e.target.value);
  };

  return <input ref={inputRef} value={text} onChange={handleChange} />;
};

export default SafeTextInput;
Output
TypeScript narrows `node` to `HTMLInputElement` inside the callback. No null checks needed downstream.
Try it live
⚠ Production Trap:
Using ref.current! in a setTimeout callback. The component unmounts before the timeout fires. current is now null. Your app crashes with 'Cannot read properties of null'. Callback refs prevent this because they run synchronously.
🎯 Key Takeaway
Favor callback refs over object refs for DOM nodes. They give you guaranteed non-null values when the node is attached.

Polymorphic 'as' Prop Pattern with ComponentPropsWithoutRef

The polymorphic 'as' prop pattern allows a component to render as different HTML elements or custom components while maintaining type safety. This is essential for reusable UI primitives like buttons, headings, or containers. Using React's ComponentPropsWithoutRef ensures that the component accepts the correct props for the rendered element. Here's how to implement it:

  1. Define a generic PolymorphicProps type that extracts props from the element type.
  2. Use React.forwardRef to forward refs with correct typing.
  3. Apply ComponentPropsWithoutRef to get props excluding ref.

Example: A Box component that can render as div, span, or section.

import React from 'react';
type BoxProps<T extends React.ElementType> = {
  as?: T;
  children?: React.ReactNode;
} & React.ComponentPropsWithoutRef<T>;
const Box = React.forwardRef(<T extends React.ElementType = 'div'>(
  props: BoxProps<T>,
  ref: React.ComponentPropsWithRef<T>['ref']
) => {
  const { as: Component = 'div', ...rest } = props;
  return <Component ref={ref} {...rest} />;
});
// Usage
<Box as="section" className="container">Content</Box>
<Box as="a" href="/link">Link</Box>

This pattern ensures that when you use as="a", the component expects href and other anchor-related props. Without ComponentPropsWithoutRef, TypeScript would not enforce these props. This is a production-grade pattern used in libraries like Chakra UI and Radix UI.

PolymorphicBox.tsxTSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';

type BoxProps<T extends React.ElementType> = {
  as?: T;
  children?: React.ReactNode;
} & React.ComponentPropsWithoutRef<T>;

const Box = React.forwardRef(<T extends React.ElementType = 'div'>(
  props: BoxProps<T>,
  ref: React.ComponentPropsWithRef<T>['ref']
) => {
  const { as: Component = 'div', ...rest } = props;
  return <Component ref={ref} {...rest} />;
});

export default Box;
Try it live
💡Use ComponentPropsWithoutRef for cleaner prop types
📊 Production Insight
In production, this pattern is widely used in design systems to create flexible components. However, be cautious with excessive polymorphism as it can increase bundle size and complexity. Use it for core primitives only.
🎯 Key Takeaway
The polymorphic 'as' prop pattern with ComponentPropsWithoutRef enables type-safe rendering of different HTML elements while preserving prop validation.

Compound Components with TypeScript: Typed Context + Namespaced Sub-Components

Compound components allow you to create a set of components that work together implicitly, like