TypeScript for React, in short
TypeScript makes React apps safer and easier to work on. You get clear errors, great autocomplete, and self‑documenting code. Start small, add types where they help, and grow from there.
Why it helps
- Catch mistakes before runtime (e.g., “object is possibly undefined”)
- Autocomplete for props, state, and API responses
- Safer refactors and clearer APIs
What to type first
- Component props and events
- API responses and form state
- Reusable utilities (with generics)
Minimal example
interface ButtonProps { label: string; onClick?: () => void }
export function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
Practical tips
- Prefer interface for objects; type for unions/aliases
- Avoid any; use unknown or generics
- Return typed results from API helpers
- Don’t over‑type; keep it readable
Bottom line
Start with props and API types, then add more where it reduces bugs and improves DX.
export function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = React.useState<T>(() => {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : initial;
});
React.useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Context with selectors
Avoid re-rendering all consumers by exposing selector hooks:
type State = { theme: "light" | "dark"; user?: { id: string; name: string } };
const AppState = React.createContext<State | null>(null);
export function useTheme() {
const ctx = React.useContext(AppState);
if (!ctx) throw new Error("useTheme must be used within provider");
return ctx.theme;
}
Component patterns
- Discriminated unions for variant-heavy components
- Template literal types for class names and tokens
- Utility types (
Pick
,Omit
,ReturnType
) to compose APIs
type Variant = "primary" | "secondary" | "ghost";
type Size = "sm" | "md" | "lg";
type ButtonProps = {
variant?: Variant;
size?: Size;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export const Button: React.FC<ButtonProps> = ({
variant = "primary",
size = "md",
...rest
}) => {
const cls = `btn ${variant} ${size}` as const;
return <button className={cls} {...rest} />;
};
Testing types
Use tsd
or expectTypeOf
to lock type contracts in libraries. In apps, favor strict options in tsconfig
and keep any
out of hot paths.
Conclusion
Type the public surface, expose narrow hooks/selectors, and use unions/generics to keep components expressive and safe.
Advanced patterns
Polymorphic components
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PolymorphicProps<C extends React.ElementType, P> = AsProp<C> &
Omit<React.ComponentPropsWithoutRef<C>, keyof AsProp<C>> &
P;
export function Text<C extends React.ElementType = "span">({
as,
children,
...rest
}: PolymorphicProps<C, { weight?: "regular" | "bold" }>) {
const Component = (as || "span") as React.ElementType;
return <Component {...rest}>{children}</Component>;
}
Exhaustiveness checks
type Status = "idle" | "loading" | "success" | "error";
function assertNever(x: never): never {
throw new Error(`Unhandled: ${x}`);
}
function statusToColor(s: Status) {
switch (s) {
case "idle":
return "gray";
case "loading":
return "blue";
case "success":
return "green";
case "error":
return "red";
default:
return assertNever(s);
}
}
Inference helpers
Use satisfies
to keep widening under control and preserve literal types.
const routes = {
home: "/",
blog: "/blog",
post: (slug: string) => `/post/${slug}`,
} as const satisfies Record<string, unknown>;