Back to React
React
medium
mid

How does TypeScript enhance React development in practice?

Typed props/state catch bugs at compile time. Generics for reusable components (`<List<T>>`) and hooks (`useState<User>`). Discriminated unions for state machines (`{ status: 'loading' } | { status: 'success', data: T }`). Editor autocomplete for prop names and event types. Refactor-safe rename. API contract typing with Zod + inferred types. Best with strict mode on and exhaustive deps lint.

8 min read·~5 min to think through

TypeScript turns React from 'works at runtime' to 'works at compile time'.

Typed props

tsx
type Props = {
  label: string;
  onClick: (id: string) => void;
  disabled?: boolean;
};

function Button({ label, onClick, disabled = false }: Props) {
  return <button onClick={() => onClick('x')} disabled={disabled}>{label}</button>;
}

Forgetting a required prop is a compile error. Mistyping the event signature is a compile error.

Typed state

tsx
const [user, setUser] = useState<User | null>(null);
setUser({ id: '1', name: 'Alice' });  // OK
setUser('not a user');                 // error

Discriminated unions for state machines

tsx
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function render(state: FetchState<User>) {
  switch (state.status) {
    case 'idle': return null;
    case 'loading': return <Spinner />;
    case 'success': return <UserCard user={state.data} />;
    case 'error': return <ErrorView error={state.error} />;
    // TS errors if we miss a case
  }
}

You can't accidentally read state.data when state is loading — the type narrows.

Generic components

tsx
function List<T>({ items, render }: {
  items: T[];
  render: (item: T) => ReactNode;
}) {
  return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}

// usage — T inferred as User
<List items={users} render={u => u.name} />

Generic hooks

tsx
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  // ...
  return data;
}

const user = useFetch<User>('/api/user'); // user: User | null

Event types

tsx
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  setValue(e.target.value);
}

function onClick(e: React.MouseEvent<HTMLButtonElement>) { ... }

Autocomplete shows valid handlers. Wrong event type is a compile error.

Children typing

tsx
type Props = { children: ReactNode };  // anything renderable
// not ReactElement — that excludes strings, arrays, null

Polymorphic components

tsx
type ButtonProps<T extends ElementType = 'button'> = {
  as?: T;
} & ComponentPropsWithoutRef<T>;

function Button<T extends ElementType = 'button'>({ as, ...rest }: ButtonProps<T>) {
  const Cmp = as ?? 'button';
  return <Cmp {...rest} />;
}

<Button as="a" href="/foo">link</Button>      // href required for anchor
<Button onClick={...}>btn</Button>            // button-only props

API contracts with Zod

tsx
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

type User = z.infer<typeof UserSchema>;

async function getUser(): Promise<User> {
  const data = await fetch('/api/me').then(r => r.json());
  return UserSchema.parse(data);  // throws if shape is wrong
}

Runtime validation + compile-time type, one source of truth.

Recommended settings

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

What TypeScript catches in React

  • Missing required props.
  • Wrong handler signatures.
  • Accessing fields that don't exist.
  • State machine cases you forgot.
  • Renaming a prop and missing a callsite.
  • Type-narrowing bugs in useReducer.

What TypeScript doesn't catch

  • Missing useEffect deps (the lint plugin does).
  • Stale closures.
  • Race conditions.
  • Performance regressions.

Senior framing

TypeScript makes refactors safe. In a 100-file codebase, renaming a prop is a 10-second task with TS and a half-day with PropTypes. The compile-time guarantee is what turns React from 'careful' to 'confident'.

Follow-up questions

  • When do you reach for generics in a hook?
  • How does Zod help typing API responses?
  • What's the difference between ReactNode and ReactElement for children typing?

Common mistakes

  • Using `any` to bypass a complaint instead of narrowing.
  • Using ReactElement for children typing — excludes strings/arrays.
  • Skipping strict mode — most of TS's value is in strict mode.

Performance considerations

  • TypeScript adds no runtime cost — it erases to JS. Compile-time cost can be significant in large monorepos; project references and incremental builds help.

Edge cases

  • Inferring generic from props sometimes requires explicit annotation.
  • Forwarding refs through polymorphic components is tricky.
  • Discriminated unions break if the discriminant isn't a literal type.

Real-world examples

  • Every modern React codebase: Linear, Vercel, Shopify, Stripe. tRPC bridges typed server + client with zero codegen. Zod + React Hook Form is the standard typed-form pattern.

Senior engineer discussion

Senior framing: TypeScript with React isn't 'static checking' — it's a design tool. State machines as discriminated unions, props as contracts, schemas as the single source of truth. The types shape the architecture.