Technology · TypeScript
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.
Typing Component Props
- Define prop types with an interface or type.
interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; } function Button({ label, onClick, disabled = false }: ButtonProps) { return <button onClick={onClick} disabled={disabled}>{label}</button>; } - Use React.FC for typed function components.
interface CardProps { title: string; children: React.ReactNode; } const Card: React.FC<CardProps> = ({ title, children }) => ( <div><h2>{title}</h2>{children}</div> ); - Export prop types so consumers can extend them.
export interface ButtonProps { label: string; onClick: () => void; } export function Button(props: ButtonProps) { } - Extend HTML element props for wrapper components.
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { label: string; } function LabeledInput({ label, ...rest }: InputProps) { return ( <label> {label} <input {...rest} /> </label> ); } - Use discriminated unions for multi-variant component props.
type AlertProps = | { variant: "success"; message: string } | { variant: "error"; message: string; code: number }; function Alert(props: AlertProps) { return <div className={props.variant}>{props.message}</div>; }
Event Handlers
- Type event handlers with React event types.
function Form() { const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { e.preventDefault(); // Handle submission }; return <form onSubmit={handleSubmit}></form>; } - Use specific event types for different inputs.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { console.log(e.target.value); }; const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => { console.log(e.button); }; - Annotate inline event handler parameters directly in JSX.
<input onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setName(e.target.value); }} /> - Type keyboard event handlers with React.KeyboardEvent.
function SearchBox() { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") submitSearch(); }; return <input onKeyDown={handleKeyDown} />; } - Pass typed event handler props for reusable 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
- Type useState with a generic parameter.
const [count, setCount] = useState<number>(0); const [user, setUser] = useState<User | null>(null); - Type useRef for DOM access.
const inputRef = useRef<HTMLInputElement>(null); function focus() { inputRef.current?.focus(); } - Type useContext with custom hooks.
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 discriminated unions.
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; } }; - Annotate custom hook return types to improve caller inference.
function useToggle(initial: boolean): [boolean, () => void] { const [value, setValue] = useState(initial); const toggle = () => setValue(v => !v); return [value, toggle]; }
Generic Components
- Build generic components for reusable logic.
interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; } function List<T>({ items, renderItem }: ListProps<T>) { return <ul>{items.map(renderItem)}</ul>; } - Constrain generics to specific shapes with extends.
interface HasId { id: string; } function useItem<T extends HasId>(item: T) { return item.id; } - Use generic components for select or dropdown inputs.
interface SelectProps<T> { options: T[]; getLabel: (option: T) => string; onChange: (selected: T) => void; } function Select<T>({ options, getLabel, onChange }: SelectProps<T>) { return ( <select onChange={(e) => onChange(options[Number(e.target.value)])}> {options.map((opt, i) => ( <option key={i} value={i}>{getLabel(opt)}</option> ))} </select> ); } - Pass default type parameters to reduce boilerplate for common cases.
interface TableProps<T = Record<string, unknown>> { rows: T[]; columns: Array<{ key: keyof T; label: string }>; } - Use React.forwardRef with generics for typed ref-forwarding components.
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( (props, ref) => <input ref={ref} {...props} /> );
Component Composition
- Type children prop explicitly for wrapper components.
interface WrapperProps { children: React.ReactNode; } function Wrapper({ children }: WrapperProps) { return <div>{children}</div>; } - Create typed render prop patterns for flexible data injection.
interface DataFetcherProps<T> { render: (data: T) => React.ReactNode; url: string; } function DataFetcher<T>({ render, url }: DataFetcherProps<T>) { const data = useFetch<T>(url); return <>{render(data)}</>; } - Use React.PropsWithChildren to add children to any props type.
function Container<T>(props: React.PropsWithChildren<{ data: T; }>) { return <div>{props.children}</div>; } - Compose higher-order component types with TypeScript generics.
function withAuth<P extends object>(Component: React.ComponentType<P>) { return function AuthWrapped(props: P) { const { user } = useAuth(); if (!user) return <Redirect to="/login" />; return <Component {...props} />; }; } - Use React.ElementType to accept any component or tag as a prop.
interface BoxProps { as?: React.ElementType; children: React.ReactNode; } function Box({ as: Tag = "div", children }: BoxProps) { return <Tag>{children}</Tag>; }
Tip: Export prop interfaces so consumers can extend or override them when needed for customization.
Warning: Avoid using
anytype — specific types catch bugs at compile time and make refactoring safer.