Back

React и TypeScript: Общие паттерны для более чистого кода

React и TypeScript: Общие паттерны для более чистого кода

При создании React-приложений с TypeScript вы будете сталкиваться с одними и теми же задачами снова и снова: как правильно типизировать props, безопасно обрабатывать события и структурировать компоненты для максимальной переиспользуемости. Эти паттерны нужны не только для удовлетворения компилятора — они помогают писать код, который легче понимать, рефакторить и поддерживать.

Эта статья охватывает наиболее практичные общие паттерны React TypeScript, которые вы будете использовать ежедневно. Вы узнаете, как эффективно типизировать компоненты, обрабатывать опциональные props, работать с событиями и refs, использовать служебные типы и применять дискриминируемые объединения в редьюсерах. Каждый паттерн включает целенаправленные примеры, показывающие именно то, как он улучшает ваш код.

Ключевые выводы

  • Явно типизируйте props и состояние с помощью интерфейсов или типов для лучшего IntelliSense и обнаружения ошибок
  • Обрабатывайте опциональные и дефолтные props с помощью опциональных свойств TypeScript и параметров по умолчанию
  • Используйте служебные типы как Pick и ComponentProps для уменьшения повторений и поддержания согласованности
  • Правильно типизируйте события и refs для типобезопасного взаимодействия с DOM
  • Используйте дискриминируемые объединения в редьюсерах для исчерпывающей обработки действий
  • Компонуйте компоненты с forwardRef для поддержания типобезопасности при передаче refs

Типизация Props и состояния

Основа паттернов React TypeScript начинается с правильной типизации props ваших компонентов. Это позволяет обнаруживать ошибки во время компиляции и обеспечивает лучшее автодополнение в редакторе.

Базовый паттерн Props

interface ButtonProps {
  label: string
  onClick: () => void
  disabled?: boolean
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  )
}

Этот паттерн гарантирует, что ваши компоненты получают правильные типы данных. Интерфейс четко документирует, какие props ожидает компонент, делая его самодокументируемым.

Типизация состояния с useState

TypeScript обычно правильно выводит типы состояния, но сложное состояние выигрывает от явной типизации:

import { useState } from 'react'

interface User {
  id: string
  name: string
  email: string
}

function UserProfile() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(false) // Тип выводится как boolean

  // TypeScript знает, что user может быть null
  return user ? <div>{user.name}</div> : <div>Loading...</div>
}

Обработка опциональных и дефолтных Props

React-компоненты часто имеют опциональные props конфигурации. Опциональные свойства TypeScript в сочетании с параметрами по умолчанию создают чистый паттерн:

interface CardProps {
  title: string
  description?: string
  variant?: 'primary' | 'secondary'
  className?: string
}

function Card({ 
  title, 
  description, 
  variant = 'primary',
  className = ''
}: CardProps) {
  return (
    <div className={`card card-${variant} ${className}`}>
      <h2>{title}</h2>
      {description && <p>{description}</p>}
    </div>
  )
}

Этот паттерн делает компоненты гибкими, сохраняя при этом типобезопасность. Обязательные props четко определены, а опциональные props имеют разумные значения по умолчанию.

Типизация событий и Refs

Обработчики событий и refs являются частыми источниками ошибок TypeScript. Эти паттерны устраняют догадки:

Паттерн обработчика событий

import { useState, ChangeEvent, FormEvent } from 'react'

interface FormProps {
  onSubmit: (data: { email: string; password: string }) => void
}

function LoginForm({ onSubmit }: FormProps) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value)
  }

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    onSubmit({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={handleEmailChange} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  )
}

Паттерн Ref

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // TypeScript знает, что inputRef.current может быть null
    inputRef.current?.focus()
  }, [])

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

Использование служебных типов

Служебные типы TypeScript уменьшают дублирование кода и поддерживают синхронизацию типов в вашем приложении.

Паттерн ComponentProps

Извлекайте props из существующих компонентов без повторения определений типов:

import { ComponentProps } from 'react'

// Получить все props, которые принимает элемент button
type ButtonProps = ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary'
}

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

// Извлечь props из ваших собственных компонентов
type MyButtonProps = ComponentProps<typeof StyledButton>

Паттерн Pick

Выбирайте конкретные props при расширении компонентов:

interface UserData {
  id: string
  name: string
  email: string
  role: string
  lastLogin: Date
}

// Нужны только некоторые свойства для карточки
type UserCardProps = Pick<UserData, 'name' | 'email'> & {
  onClick?: () => void
}

function UserCard({ name, email, onClick }: UserCardProps) {
  return (
    <div onClick={onClick}>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

Композиция компонентов с forwardRef

При создании библиотек компонентов или обертывании нативных элементов forwardRef поддерживает типобезопасность, предоставляя refs:

import { forwardRef, useRef, InputHTMLAttributes } from 'react'

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string
  error?: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} {...props} />
        {error && <span className="error">{error}</span>}
      </div>
    )
  }
)

Input.displayName = 'Input'

// Использование сохраняет полную типобезопасность
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  return <Input ref={inputRef} label="Email" type="email" />
}

Дискриминируемые объединения в редьюсерах

Дискриминируемые объединения делают действия редьюсера типобезопасными и обеспечивают исчерпывающую проверку:

interface TodoState {
  items: Array<{ id: string; text: string; done: boolean }>
  filter: 'all' | 'active' | 'completed'
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string }
  | { type: 'SET_FILTER'; payload: TodoState['filter'] }
  | { type: 'CLEAR_COMPLETED' }

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        items: [...state.items, {
          id: Date.now().toString(),
          text: action.payload,
          done: false
        }]
      }
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, done: !item.done }
            : item
        )
      }
    
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        items: state.items.filter(item => !item.done)
      }
    
    default:
      // TypeScript гарантирует обработку всех случаев
      const exhaustive: never = action
      return state
  }
}

Этот паттерн гарантирует обработку всех типов действий. Если вы добавите новый тип действия, TypeScript выдаст ошибку, пока вы не обработаете его в редьюсере.

Обновления React 19

С React 19 новые возможности, такие как Actions (useActionState, useFormStatus), API use() для потребления промисов или контекста, и первоклассная поддержка серверных компонентов делают типобезопасность еще более важной. При типизации Actions явно определяйте форму FormData и возвращаемое значение действия, чтобы серверный и клиентский код оставались синхронизированными. Предстоящий переход от forwardRef к передаче ref как стандартного prop также упрощает типизацию — обрабатывайте ref как любой другой prop и позвольте TypeScript обеспечить корректность через границы компонентов.

Заключение

Эти общие паттерны React TypeScript формируют основу типобезопасных React-приложений. Явно типизируя props и состояние, правильно обрабатывая события и refs, и используя служебные типы TypeScript, вы создаете код, который является одновременно более безопасным и поддерживаемым. Паттерны работают вместе — правильная типизация props обеспечивает лучшую композицию компонентов, в то время как дискриминируемые объединения гарантируют исчерпывающую обработку в сложной логике состояния.

Начните с базовой типизации props и постепенно внедряйте служебные типы и продвинутые паттерны по мере роста вашего приложения. Инвестиции в правильную типизацию окупаются через лучшую поддержку IDE, меньше ошибок времени выполнения и более четкие контракты компонентов.

Часто задаваемые вопросы

Используйте interface, когда вам может понадобиться расширить или объединить объявления позже. Используйте type для объединений, пересечений или при работе со служебными типами. Оба варианта подходят для базовых props, поэтому выберите один подход и придерживайтесь его.

Используйте ReactNode для максимальной гибкости: children: ReactNode. Это принимает строки, числа, элементы, фрагменты и массивы. Для более конкретных случаев используйте ReactElement или конкретные типы элементов.

Используйте обобщенные функции, которые сохраняют типы props исходного компонента, добавляя новые. Служебный тип ComponentProps помогает извлекать и расширять существующие типы компонентов без повторений.

Типизируйте параметры функций, возвращаемые типы для экспортируемых функций и сложное состояние явно. Позвольте TypeScript выводить простые случаи, такие как useState с примитивами или очевидные обработчики событий, чтобы уменьшить многословность.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers