12k
All articles

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

介绍如何为 React 组件的 props 定义类型、使用 refs 处理事件,以及运用实用的 TypeScript 工具类型,使组件更安全、易于维护。

OpenReplay Team
OpenReplay Team
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?

当你可能需要稍后扩展或合并声明时使用 interface。当处理联合类型、交叉类型或使用工具类型时使用 type。对于基础 props,两者都可以,选择一种方法并保持一致。

如何正确地为 children 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

We use cookies to improve your experience. By using our site, you accept cookies.