Technology · TypeScript
TypeScript With React Patterns
Common TypeScript patterns for React components, hooks, and props.
TL;DR
- 01Type props with interfaces or types for component APIs.
- 02Use React.FC<Props> or function return types for components.
- 03Extend event types from React for type-safe handlers.
Component Props
- Type component props 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 function components with TypeScript.
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 native 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 components with multiple prop shapes.
type BadgeProps = | { variant: "count"; count: number } | { variant: "dot" }; function Badge(props: BadgeProps) { return props.variant === "count" ? <span>{props.count}</span> : <span className="dot" />; }
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 input elements.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { console.log(e.target.value); }; const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => { console.log(e.button); }; - Annotate inline handler parameters directly in JSX.
<input onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setName(e.target.value); }} /> - Handle keyboard events with React.KeyboardEvent.
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") submitForm(); }; - Define event handler prop types for reusable form components.
interface SearchProps { onSearch: (query: string, e: React.FormEvent<HTMLFormElement>) => void; } function SearchForm({ onSearch }: SearchProps) { return ( <form onSubmit={(e) => { e.preventDefault(); onSearch((e.currentTarget.elements[0] as HTMLInputElement).value, e); }}> <input type="text" /><button type="submit">Search</button> </form> ); }
Hooks with TypeScript
- Type useState with 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 hook.
const UserContext = React.createContext<User | undefined>(undefined); function useUser() { const context = useContext(UserContext); if (!context) throw new Error("useUser must be in 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 explicitly.
function useCounter(initial: number): [number, () => void, () => void] { const [count, setCount] = useState(initial); const inc = () => setCount(c => c + 1); const dec = () => setCount(c => c - 1); return [count, inc, dec]; }
Component Composition
- Type children prop explicitly for wrapper components.
interface WrapperProps { children: React.ReactNode; } function Wrapper({ children }: WrapperProps) { return <div>{children}</div>; } - Type render props and function children patterns.
interface RenderProps<T> { render: (data: T) => React.ReactNode; } function DataFetcher<T>({ render }: RenderProps<T>) { const data = useData<T>(); return <>{render(data)}</>; } - Create reusable component type helpers.
type ComponentWithChildren<P = {}> = React.FC<P & { children?: React.ReactNode; }>; - Use higher-order components with typed wrappers.
function withLogger<P extends object>(Component: React.ComponentType<P>) { return function Logged(props: P) { console.log("render", Component.displayName); return <Component {...props} />; }; } - Accept polymorphic as prop to render different HTML tags.
interface BoxProps { as?: React.ElementType; children: React.ReactNode; } function Box({ as: Tag = "div", children }: BoxProps) { return <Tag>{children}</Tag>; }
Generic Components
- Build generic components for reusable list or table 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 React.PropsWithChildren to add children to generic props.
function Container<T>(props: React.PropsWithChildren<{ data: T; }>) { return <div>{props.children}</div>; } - Build generic select components for any option type.
interface SelectProps<T> { options: T[]; getLabel: (opt: T) => string; value: T; onChange: (val: T) => void; } function Select<T>({ options, getLabel, value, 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> ); } - Use React.forwardRef with generics for typed forwarded-ref components.
const Input = React.forwardRef< HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement> >((props, ref) => <input ref={ref} {...props} />);
Tip: Define prop interfaces at the top level and export them so consumers can extend or override props when needed.
Warning: Avoid over-typing or using
any— specific types prevent bugs and make refactoring safer.