React 与 TypeScript:编写更清洁代码的常用模式

在使用 TypeScript 构建 React 应用程序时,你会反复遇到相同的挑战:如何正确地为 props 定义类型、安全地处理事件,以及构建具有最大可重用性的组件。这些模式不仅仅是为了满足编译器的要求——它们是为了编写更易于理解、重构和维护的代码。
本文涵盖了你在日常开发中最实用的 React TypeScript 常用模式。你将学习如何有效地为组件定义类型、处理可选 props、使用事件和 refs、利用工具类型,以及在 reducers 中使用可区分联合类型。每个模式都包含重点示例,准确展示为什么它能改善你的代码。
核心要点
- 明确地为 props 和 state 定义类型,使用接口或类型以获得更好的 IntelliSense 和错误捕获
- 处理可选和默认 props,使用 TypeScript 的可选属性和默认参数
- 使用工具类型如
Pick
和ComponentProps
来减少重复并保持一致性 - 正确地为事件和 refs 定义类型以实现类型安全的 DOM 交互
- 在 reducers 中利用可区分联合类型实现详尽的 action 处理
- 使用 forwardRef 组合组件在传递 refs 时保持类型安全
为 Props 和 State 定义类型
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 为 State 定义类型
TypeScript 通常能正确推断 state 类型,但复杂的 state 受益于明确的类型定义:
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'
// 获取 button 元素接受的所有 props
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" />
}
在 Reducers 中使用可区分联合类型
可区分联合类型使 reducer actions 类型安全并启用详尽检查:
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
}
}
这种模式确保你处理所有 action 类型。如果你添加新的 action 类型,TypeScript 会报错,直到你在 reducer 中处理它。
React 19 更新
随着 React 19 的发布,新功能如 Actions(useActionState
、useFormStatus
)、用于消费 promises 或 context 的 use()
API,以及对服务器组件的一流支持,使得类型安全变得更加重要。在为 Actions 定义类型时,明确定义 FormData
的形状和 action 的返回值,以便服务器和客户端代码保持同步。即将到来的从 forwardRef
转向将 ref
作为标准 prop 传递的变化也简化了类型定义——将 ref
视为任何其他 prop,让 TypeScript 确保组件边界的正确性。
结论
这些 React TypeScript 常用模式构成了类型安全 React 应用程序的基础。通过明确地为 props 和 state 定义类型、正确处理事件和 refs,以及利用 TypeScript 的工具类型,你创建的代码既更安全又更易于维护。这些模式协同工作——正确的 prop 类型定义支持更好的组件组合,而可区分联合类型确保复杂状态逻辑的详尽处理。
从基础的 prop 类型定义开始,随着应用程序的增长逐步引入工具类型和高级模式。在正确类型定义上的投资通过更好的 IDE 支持、更少的运行时错误和更清晰的组件契约得到回报。
常见问题
当你可能需要稍后扩展或合并声明时使用 interface。当处理联合类型、交叉类型或使用工具类型时使用 type。对于基础 props,两者都可以,选择一种方法并保持一致。
使用 ReactNode 以获得最大灵活性:children: ReactNode。这接受字符串、数字、元素、片段和数组。对于更具体的情况,使用 ReactElement 或特定的元素类型。
使用泛型函数来保留原始组件的 prop 类型,同时添加新的类型。ComponentProps 工具类型有助于提取和扩展现有组件类型而无需重复。
为函数参数、导出函数的返回类型和复杂 state 明确定义类型。让 TypeScript 推断简单情况,如带有原始类型的 useState 或明显的事件处理程序,以减少冗长。