Типизация React-компонентов

Типизация React-компонентов в TypeScript — описание интерфейса Props, типов state и хуков, что позволяет проверять корректность передаваемых данных, автодополнять props в JSX и безопасно рефакторить компоненты.

Зачем нужно

Без типизации props ошибки (передача number вместо string, пропуск обязательного prop) ловятся только в рантайме. TypeScript проверяет props при написании JSX, что исключает целый класс ошибок.

Где используется

  • Function components с props
  • Компоненты с generic данными: списки, таблицы, формы
  • Хуки: useState, useReducer, useContext
  • Forwardref компоненты
  • HOC и render props паттерны

Основной контент

Function component с props

import { FC, ReactNode } from "react";

interface ButtonProps {
  label: string;
  onClick:  => void;
  disabled?: boolean;
  variant?: "primary" | "secondary" | "danger";
  children?: ReactNode;
}

// Вариант 1: FC<Props> (менее предпочтителен — скрывает возвращаемый тип)
const Button: FC<ButtonProps> = ({ label, onClick, disabled, variant = "primary" }) => (
  <button onClick={onClick} disabled={disabled} className={`btn-${variant}`}>
    {label}
  </button>
);

// Вариант 2: явная типизация параметра (предпочтительно)
function Button({ label, onClick, disabled = false, variant = "primary" }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled} className={`btn-${variant}`}>
      {label}
    </button>
  );
}

Компонент с children

import { ReactNode, PropsWithChildren } from "react";

// Вариант 1: явный children в Props
interface CardProps {
  title: string;
  children: ReactNode;
}

// Вариант 2: PropsWithChildren утилита
type CardProps2 = PropsWithChildren<{ title: string }>;

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

Generic компонент

interface ListProps<T> {
  items: T;
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T) => string;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage = "No items" }: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}

// Использование — TypeScript выводит T из items
<List
  items={users}
  keyExtractor={(u) => u.id}
  renderItem={(u) => <span>{u.name}</span>}
/>

useState и useReducer

import { useState, useReducer } from "react";

// useState — тип выводится из начального значения
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string

// Явный тип для nullable
const [user, setUser] = useState<User | null>(null);

// useReducer с typed action
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset"; payload: number };

function counterReducer(state: number, action: Action): number {
  switch (action.type) {
    case "increment": return state + 1;
    case "decrement": return state - 1;
    case "reset":     return action.payload;
  }
}

const [count2, dispatch] = useReducer(counterReducer, 0);
dispatch({ type: "reset", payload: 10 }); // типобезопасно

Ref и forwardRef

import { useRef, forwardRef, Ref } from "react";

// Ref на DOM элемент
function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focus = () => inputRef.current?.focus();

  return <input ref={inputRef} type="search" />;
}

// forwardRef
interface InputProps {
  placeholder?: string;
  onChange: (value: string) => void;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ placeholder, onChange }, ref) => (
    <input
      ref={ref}
      placeholder={placeholder}
      onChange={(e) => onChange(e.target.value)}
    />
  )
);

Частые ошибки

  • Использовать React.FC повсеместноFC делает children неявно опциональным в старых версиях и скрывает возвращаемый тип; лучше явная аннотация параметра.
  • Передавать any вместо ReactNode — для children используйте ReactNode, для элемента — ReactElement, для рендерабельного — ReactNode.
  • Не типизировать event handlers в JSXonClick={handler} — TypeScript выведет тип из пропа элемента; явный тип нужен только для отдельных функций.
  • Инициализировать useState без типа при nulluseState(null) даёт тип null, не User | null; нужен явный generic.

Связанные темы

Ресурсы