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.
TypeScript turns React from 'works at runtime' to 'works at compile time'.
Typed props
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
const [user, setUser] = useState<User | null>(null);
setUser({ id: '1', name: 'Alice' }); // OK
setUser('not a user'); // errorDiscriminated unions for state machines
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
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
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
// ...
return data;
}
const user = useFetch<User>('/api/user'); // user: User | nullEvent types
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
type Props = { children: ReactNode }; // anything renderable
// not ReactElement — that excludes strings, arrays, nullPolymorphic components
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 propsAPI contracts with Zod
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
{
"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.