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 с примитивами или очевидные обработчики событий, чтобы уменьшить многословность.