Средний 12 мин чтения

React + TypeScript

Типизация React-компонентов: FC, Props, generics, useState, useRef, useRef typing, event handlers и полезные паттерны.

ReactTypeScriptFCPropsтипизацияgenerics

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

Function Components (FC)

type Props = {
  title: string
  count: number
}

function Header({ title, count }: Props) {
  return <h1>{title} ({count})</h1>
}

// Альтернатива через FC
const Header: React.FC<Props> = ({ title, count }) => {
  return <h1>{title} ({count})</h1>
}

В современной практике FC используют реже — обычная функция с типизированным props понятнее и гибче.

Children

type CardProps = {
  children: React.ReactNode
  title?: string
}

function Card({ children, title }: CardProps) {
  return (
    <div className="border rounded p-4">
      {title && <h2>{title}</h2>}
      {children}
    </div>
  )
}

// Использование
<Card title="Профиль">
  <p>Содержимое карточки</p>
</Card>

Расширение HTML-элементов

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary'
}

function Button({ variant = 'primary', className, ...rest }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} ${className ?? ''}`}
      {...rest}
    />
  )
}

// Все нативные атрибуты button доступны: onClick, disabled, type и т.д.
<Button type="submit" disabled={false} onClick={() => {}}>
  Отправить
</Button>

useState

const [name, setName] = useState<string>('')

const [user, setUser] = useState<{ name: string; age: number } | null>(null)

const [items, setItems] = useState<string[]>([])

// TypeScript выводит тип из начального значения — аннотация не нужна
const [count, setCount] = useState(0)       // number
const [isOpen, setIsOpen] = useState(false) // boolean

// Но для null/undefined начального значения тип нужен
const [data, setData] = useState<User | null>(null)

Обновление через функцию

setUser((prev) => ({
  ...prev!,
  name: 'Иван',
}))

useRef

// Ссылка на DOM-элемент
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
  inputRef.current?.focus()
}, [])

return <input ref={inputRef} />

// Ссылка на произвольное значение (мутабельное)
const timerRef = useRef<number | null>(null)

useEffect(() => {
  timerRef.current = window.setTimeout(() => { /* ... */ }, 1000)
  return () => {
    if (timerRef.current) clearTimeout(timerRef.current)
  }
}, [])

useRef<number> создаёт MutableRefObject<number | undefined>. Если нужно строго без undefined — передайте начальное значение:

const countRef = useRef(0)   // MutableRefObject<number>

Обработчики событий

function Form() {
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    console.log(e.target.value)
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
  }

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    console.log('Клик')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Отправить</button>
    </form>
  )
}

Основные типы событий:

СобытиеТип
onChangeReact.ChangeEvent<HTMLInputElement>
onClickReact.MouseEvent<HTMLButtonElement>
onSubmitReact.FormEvent<HTMLFormElement>
onKeyDownReact.KeyboardEvent<HTMLInputElement>
onFocusReact.FocusEvent<HTMLInputElement>

Props с generic-компонентами

type ListProps<T> = {
  items: T[]
  renderItem: (item: T) => React.ReactNode
  keyExtractor: (item: T) => string | number
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  )
}

// Использование
<List
  items={[{ id: 1, name: 'Иван' }, { id: 2, name: 'Мария' }]}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

forwardRef

Для передачи ref в дочерний компонент:

type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...rest }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} {...rest} />
      </div>
    )
  },
)

Input.displayName = 'Input'

// Использование
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)
  return <Input ref={inputRef} label="Email" />
}

ComponentProps — извлечение типов

// Получить тип props компонента
type ButtonProps = React.ComponentProps<typeof Button>

// Получить тип props HTML-элемента
type DivProps = React.ComponentProps<'div'>

// ComponentPropsWithRef включает ref
type InputProps = React.ComponentPropsWithRef<'input'>

Паттерн: discriminated union для варианта компонента

type AlertProps =
  | { variant: 'success'; message: string }
  | { variant: 'error'; error: Error }
  | { variant: 'warning'; warnings: string[] }

function Alert(props: AlertProps) {
  switch (props.variant) {
    case 'success':
      return <div className="text-green-600">{props.message}</div>
    case 'error':
      return <div className="text-red-600">{props.error.message}</div>
    case 'warning':
      return (
        <ul className="text-yellow-600">
          {props.warnings.map((w) => <li key={w}>{w}</li>)}
        </ul>
      )
  }
}

TypeScript гарантирует, что в каждом case доступны только соответствующие поля.

Типизация кастомных хуков

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key)
    return stored ? (JSON.parse(stored) as T) : initialValue
  })

  function setStored(newValue: T) {
    setValue(newValue)
    localStorage.setItem(key, JSON.stringify(newValue))
  }

  return [value, setStored] as const
}

// Использование
const [name, setName] = useLocalStorage<string>('name', '')
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')

Итог

Типизация React — это в первую очередь типизация props и событий. React.ButtonHTMLAttributes, React.ChangeEvent, React.ComponentProps покрывают большинство случаев. Для сложных вариантов используйте discriminated unions и generics. Чем точнее типы — тем меньше багов.