Technology · TypeScript

TypeScript With React Patterns

Common TypeScript patterns for React components, hooks, and props.

TL;DR
  1. 01Type props with interfaces or types for component APIs.
  2. 02Use React.FC<Props> or function return types for components.
  3. 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.

TypeScript Strict ModeTypeScript With React