Back

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

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

在使用 TypeScript 构建 React 应用程序时,你会反复遇到相同的挑战:如何正确地为 props 定义类型、安全地处理事件,以及构建具有最大可重用性的组件。这些模式不仅仅是为了满足编译器的要求——它们是为了编写更易于理解、重构和维护的代码。

本文涵盖了你在日常开发中最实用的 React TypeScript 常用模式。你将学习如何有效地为组件定义类型、处理可选 props、使用事件和 refs、利用工具类型,以及在 reducers 中使用可区分联合类型。每个模式都包含重点示例,准确展示为什么它能改善你的代码。

核心要点

  • 明确地为 props 和 state 定义类型,使用接口或类型以获得更好的 IntelliSense 和错误捕获
  • 处理可选和默认 props,使用 TypeScript 的可选属性和默认参数
  • 使用工具类型PickComponentProps 来减少重复并保持一致性
  • 正确地为事件和 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(useActionStateuseFormStatus)、用于消费 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 或明显的事件处理程序,以减少冗长。

Listen to your bugs 🧘, with OpenReplay

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