TypeScript with React Cheat Sheet
Covers typing React function components, useState/useReducer generics, DOM and synthetic event types, useRef and forwardRef, and common utility types.
Typing Function Components
A props interface with an optional union prop.
interface ButtonProps { label: string; onClick: () => void; variant?: 'primary' | 'secondary'; // optional prop with a union type children?: React.ReactNode;}function Button({ label, onClick, variant = 'primary', children }: ButtonProps) { return ( <button className={`btn-${variant}`} onClick={onClick}> {label} {children} </button> );}
useState & useReducer Generics
Explicit typing when inference isn't enough.
// Explicit generic when the initial value doesn't convey the full typeconst [user, setUser] = useState<User | null>(null);const [count, setCount] = useState(0); // inferred as number// useReducer with a discriminated union of actionstype Action = | { type: 'increment'; payload: number } | { type: 'reset' };function reducer(state: number, action: Action): number { switch (action.type) { case 'increment': return state + action.payload; case 'reset': return 0; }}const [state, dispatch] = useReducer(reducer, 0);
Typing Events
Synthetic event types for common handlers.
function SearchInput() { const [value, setValue] = useState(''); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value); }; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); }; const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { console.log('clicked', e.currentTarget); }; return ( <form onSubmit={handleSubmit}> <input value={value} onChange={handleChange} /> <button onClick={handleClick}>Go</button> </form> );}
useRef & forwardRef
DOM refs, mutable value refs, and ref forwarding.
// DOM ref: initialize with null, TS narrows to HTMLInputElement | nullconst inputRef = useRef<HTMLInputElement>(null);useEffect(() => { inputRef.current?.focus(); }, []);// Mutable value ref (no DOM node): initialize with the value directlyconst renderCount = useRef(0);renderCount.current += 1;// forwardRef takes two generics: <RefType, PropsType>const FancyInput = forwardRef<HTMLInputElement, { placeholder?: string }>( (props, ref) => <input ref={ref} {...props} />);
Utility Types & Patterns
Reach for these before duplicating a prop type.
- React.ComponentProps<typeof Comp>- Extracts an existing component's prop types, useful for wrapper components
- React.PropsWithChildren<T>- Adds an optional children: ReactNode to a props type
- Partial<T> / Pick<T,K> / Omit<T,K>- Built-in TS utilities for deriving variant prop types without duplication
- as const- Narrows array/object literals to readonly tuple/literal types, useful for discriminated unions
- satisfies- Validates a literal against a type without widening it, handy for config objects
Generic Components
A reusable List component that infers the item type from the data prop.
type ListProps<T> = { items: T[]; renderItem: (item: T) => React.ReactNode; keyExtractor: (item: T) => string | number;};function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { return ( <ul> {items.map((item) => ( <li key={keyExtractor(item)}>{renderItem(item)}</li> ))} </ul> );}// T is inferred as User here, no manual generic argument needed<List items={users} keyExtractor={(u) => u.id} renderItem={(u) => <span>{u.name}</span>} />;
Discriminated Unions for Render State
Exhaustive, type-safe handling of async/UI state without optional-field guesswork.
type FetchState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error };function render<T>(state: FetchState<T>) { switch (state.status) { case 'idle': return null; case 'loading': return 'Loading...'; case 'success': return state.data; // TS knows .data exists here case 'error': return state.error.message; // TS knows .error exists here default: { const _exhaustive: never = state; // compile error if a case is missing return _exhaustive; } }}
Typed Context with a Custom Hook Guard
Avoids sprinkling non-null assertions or optional chaining at every consumer.
interface ThemeContextValue { theme: 'light' | 'dark'; toggle: () => void;}// undefined as the default forces consumers through the guarded hookconst ThemeContext = createContext<ThemeContextValue | undefined>(undefined);export function useTheme(): ThemeContextValue { const ctx = useContext(ThemeContext); if (!ctx) throw new Error('useTheme must be used within a ThemeProvider'); return ctx; // return type is narrowed to ThemeContextValue, no `| undefined`}export function ThemeProvider({ children }: React.PropsWithChildren) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const value = useMemo( () => ({ theme, toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')) }), [theme] ); return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;}
Polymorphic Components with an `as` Prop
Lets a component render as a different element/tag while keeping props type-safe for that element.
type PolymorphicProps<E extends React.ElementType> = { as?: E; children?: React.ReactNode;} & Omit<React.ComponentPropsWithoutRef<E>, 'as' | 'children'>;function Text<E extends React.ElementType = 'span'>({ as, children, ...rest}: PolymorphicProps<E>) { const Component = as || 'span'; return <Component {...rest}>{children}</Component>;}// href is type-checked because as='a' pulls in anchor props<Text as="a" href="/docs">Docs</Text>;<Text as="button" onClick={() => {}}>Click</Text>;
Advanced Typing Patterns
Reach for these once basic prop typing feels routine.
- never in exhaustiveness checks- Assigning a switch's default case to a never-typed variable makes the compiler flag unhandled union members
- React.ComponentPropsWithoutRef<E>- Props for a JSX element/component type minus ref, the basis for polymorphic 'as' components
- Generic component constraints- <T extends { id: string }> restricts a generic prop's shape while keeping the concrete type inferred
- Discriminated union props- A shared literal field (e.g. status/variant) lets TS narrow the rest of the object's shape per branch
- ReturnType<typeof useMyHook>- Derives a type from a hook's return value instead of hand-writing a duplicate interface
- Template literal prop types- `size?: `col-${1|2|3|4|6|12}`` constrains string props to a defined pattern at compile time
Skip React.FC for new components — it forces an implicit children prop even when a component doesn't accept any, and makes generic components awkward to type. Just type the props argument directly: function Comp(props: Props) { ... }.