TypeScript With React
Type React components, props, and hooks safely with TypeScript.
TL;DR
- 01Define prop interfaces or types for component APIs.
- 02Use React.FC<Props> or function return types for components.
- 03Extend built-in React types for events and refs.
Tips
- 01Export prop interfaces so consumers can extend or override them when needed for customization.
Warnings
- 01Avoid using the any type — specific types catch bugs at compile time and make refactoring safer.
Typing Component Props
Define props with an interface — use ? for optional props with defaults.
interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; } function Button({ label, onClick, disabled = false }: ButtonProps) { return <button onClick={onClick} disabled={disabled}>{label}</button>; }Use type instead of interface when you need union or intersection props.
type CardProps = { title: string; variant: "primary" | "secondary"; }; function Card({ title, variant }: CardProps) { return <div className={variant}><h2>{title}</h2></div>; }Export prop interfaces so consumers can extend or build on them.
export interface ButtonProps { label: string; onClick: () => void; } export function Button(props: ButtonProps) { /* ... */ }Extend HTML element attributes to allow all native props on wrappers.
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { label: string; } function LabeledInput({ label, ...rest }: InputProps) { return <label>{label}<input {...rest} /></label>; }Use React.ComponentPropsWithoutRef to forward all props of a native tag.
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> { variant: "primary" | "ghost"; } function Button({ variant, ...rest }: ButtonProps) { return <button className={variant} {...rest} />; }
Event Handlers
Type a change handler for text inputs using React.ChangeEvent.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setName(e.target.value); // e.target.value is string };Type a form submit handler using React.FormEventHandler.
function Form() { const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { e.preventDefault(); // handle form data }; return <form onSubmit={handleSubmit}></form>; }Type a mouse click handler using React.MouseEventHandler.
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => { console.log(e.button); // 0 = left, 1 = middle, 2 = right };Type a keyboard handler to respond to specific key presses.
function SearchBox() { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") submitSearch(); }; return <input onKeyDown={handleKeyDown} />; }Define typed handler props in an interface for reusable form components.
interface TableRowProps { onRowClick: (id: number, e: React.MouseEvent<HTMLTableRowElement>) => void; } function TableRow({ onRowClick }: TableRowProps) { return <tr onClick={(e) => onRowClick(1, e)}></tr>; }
Typing Hooks
Pass a union type to useState when the initial value can be null.
const [count, setCount] = useState<number>(0); const [user, setUser] = useState<User | null>(null);Type useRef for DOM elements and access it with optional chaining.
const inputRef = useRef<HTMLInputElement>(null); function focus() { inputRef.current?.focus(); // safe — current may be null }Type createContext with a default and wrap it in a typed custom hook.
const UserContext = React.createContext<User | undefined>(undefined); function useUser() { const context = useContext(UserContext); if (!context) throw new Error("useUser needs provider"); return context; }Type useReducer with a discriminated union to cover every action shape.
type Action = | { type: "INCREMENT"; payload: number } | { type: "DECREMENT"; payload: number }; const reducer = (state: number, action: Action) => { switch (action.type) { case "INCREMENT": return state + action.payload; case "DECREMENT": return state - action.payload; } };Use as const on the return array to get a precise tuple type from a custom hook.
function useToggle(initial: boolean) { const [value, setValue] = useState(initial); const toggle = () => setValue(v => !v); return [value, toggle] as const; // [boolean, () => void] }
Children and Refs
Use React.ReactNode for children props — it accepts JSX, strings, or null.
interface WrapperProps { children: React.ReactNode; } function Wrapper({ children }: WrapperProps) { return <div className="wrapper">{children}</div>; }Use React.PropsWithChildren to add children to any existing props type.
interface CardProps { title: string; } function Card({ title, children }: React.PropsWithChildren<CardProps>) { return <div><h2>{title}</h2>{children}</div>; }Use useRef with a DOM type and access .current safely with optional chaining.
const videoRef = useRef<HTMLVideoElement>(null); function play() { videoRef.current?.play(); // optional chain handles null }Use React.forwardRef with explicit generic types for ref-forwarding components.
const TextInput = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( (props, ref) => <input ref={ref} {...props} /> );Distinguish RefObject (read-only current) from MutableRefObject (writable current).
// React.RefObject<T>: current is readonly — for DOM refs via useRef(null) const domRef: React.RefObject<HTMLDivElement> = useRef(null); // React.MutableRefObject<T>: current is writable — for storing values const timerRef: React.MutableRefObject<number> = useRef(0);
TypeScript Config for React
Set jsx to react-jsx to avoid importing React in every file.
{ "compilerOptions": { "jsx": "react-jsx" } }Enable strict mode to activate all strict type checks at once.
{ "compilerOptions": { "strict": true } }Enable noUncheckedIndexedAccess to catch undefined array access at compile time.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } } // items[0] is now string | undefined instead of stringUse paths to create tsconfig aliases for cleaner imports across large projects.
{ "compilerOptions": { "paths": { "@components/*": ["./src/components/*"], "@lib/*": ["./src/lib/*"] } } }Set composite: true to enable project references in monorepo packages.
{ "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true } }
FAQ
Use React.ChangeEvent
React.FC implicitly typed children in older React versions and can hide return type mismatches, so many teams prefer plain functions with an explicit return type of JSX.Element or React.ReactElement. Either approach works — pick one and stay consistent across the codebase.
Pass a union type as the generic: useState<User | null>(null). TypeScript will then enforce null checks before you access properties on the state value.
Add a type parameter to the function signature: function List
Extend the corresponding React HTML attribute type: interface ButtonProps extends React.ButtonHTMLAttributes