Back

TSX 与类型化前端组件的兴起

TSX 与类型化前端组件的兴起

你正在构建一个 React 组件。你在应该传递数字的地方传递了字符串。TypeScript 在浏览器之前就捕获了这个错误。这种简单的交互——类型防止运行时错误——解释了为什么 TSX 已成为现代前端开发的默认格式。

本文涵盖了类型化前端组件在实践中的实际含义:props 类型、事件处理、children 模式,以及 TypeScript 类型推断如何消除样板代码。我们还将讨论 React 19 的服务端和客户端组件分离如何影响你的类型策略。

核心要点

  • TSX 类型化组件在编译时捕获类型不匹配,在代码到达浏览器之前减少运行时错误。
  • TypeScript 类型推断自动处理大多数事件类型——只有在将处理程序提取到单独函数时才需要显式注解。
  • 可辨识联合类型使具有互斥行为的组件类型安全,TypeScript 根据判别属性收窄类型。
  • React 19 的服务端/客户端边界引入了序列化约束:从服务端组件传递到客户端组件的 props 必须可序列化(不能是函数、类、Symbol 或其他不可序列化的值)。

类型化组件的实际含义

类型化组件不仅仅是带有类型化 props 的组件。它是 TypeScript 理解整个契约的组件:输入什么、输出什么,以及中间发生什么。

使用现代 JSX 转换(react-jsx),你不需要在每个文件中导入 React。你编写 TSX,工具链处理其余部分。Vite 开箱即用地正确配置了这一点:

interface ButtonProps {
  label: string
  disabled?: boolean
}

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

注意这里没有 React.FC。这种模式是可选的——而且通常是不必要的。直接在函数参数上对 props 进行类型注解更简洁,并避免了 React.FC 添加的隐式 children prop,无论你是否需要它。

Props 类型与 React TSX 类型安全

Props 类型是大多数开发者的起点,但 React TSX 类型安全更深入。考虑使用可辨识联合类型来处理具有互斥行为的组件:

type LinkButtonProps = 
  | { variant: 'button'; onClick: () => void }
  | { variant: 'link'; href: string }

function LinkButton(props: LinkButtonProps) {
  if (props.variant === 'link') {
    return <a href={props.href}>Navigate</a>
  }
  return <button onClick={props.onClick}>Click</button>
}

TypeScript 根据 variant 收窄类型。当 variant'button' 时,你不会意外访问 href。这种模式对于复杂的组件 API 具有良好的扩展性。

无样板代码的事件类型

事件处理程序经常让 TypeScript 新手感到困惑。好消息是:类型推断自动处理大多数情况。

import { useState } from 'react'

function SearchInput() {
  const [query, setQuery] = useState('')

  // TypeScript 从上下文推断事件类型
  return (
    <input 
      value={query} 
      onChange={(e) => setQuery(e.currentTarget.value)} 
    />
  )
}

当你将处理程序提取到单独的函数时,需要显式类型:

import type { ChangeEvent } from 'react'

function handleChange(event: ChangeEvent<HTMLInputElement>) {
  // event.currentTarget.value 被正确类型化为 string
}

Children 类型:ReactNode vs ReactElement

对于 children,React.ReactNode 涵盖了常见情况——任何可渲染的内容:

import type { ReactNode } from 'react'

interface CardProps {
  children: ReactNode
}

当你需要严格的 JSX 元素时使用 React.ReactElement,排除字符串和数字。对于接受 children 的组件,PropsWithChildren 是手动添加 children prop 的更简洁替代方案:

import type { PropsWithChildren } from 'react'

interface CardProps {
  title: string
}

function Card({ title, children }: PropsWithChildren<CardProps>) {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  )
}

React 19 中的服务端和客户端组件类型

React 19 TypeScript 项目,特别是使用 Next.js App Router 的项目,将组件分为服务端和客户端边界。这会影响类型。

服务端组件可以是异步的并直接获取数据:

// 服务端组件 - 不需要指令(在 App Router 中是默认的)
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId)
  return <div>{user.name}</div>
}

客户端组件需要 'use client' 指令并可以使用 hooks:

'use client'

import { useState } from 'react'

interface CounterProps {
  initialCount: number
}

function Counter({ initialCount }: CounterProps) {
  const [count, setCount] = useState(initialCount)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

类型边界很重要:从服务端组件传递到客户端组件的 props 必须可序列化。函数、类、Symbol 和不可序列化的值(如类实例或 Date)无法跨越该边界。

表单操作与现代类型化工作流

React 19 引入了服务端操作(通常与表单一起使用)作为处理提交的类型化模式,通常在服务端文件中定义并从客户端组件调用:

async function submitForm(formData: FormData) {
  'use server'
  const email = formData.get('email')
  if (typeof email !== 'string') {
    throw new Error('Email is required')
  }
  // 处理提交
}

这通过保持从客户端到服务端的表单处理类型感知,与类型化前端组件集成。

结论

TSX 类型化组件通过在编译时捕获不匹配来减少错误。TypeScript 类型推断最小化样板代码——你很少需要为 hooks 或内联处理程序提供显式类型注解。React 19 中的服务端/客户端分离增加了新的考虑因素:序列化边界。

除非有特定原因,否则跳过 React.FC。直接对 props 进行类型注解。让类型推断发挥作用。你的编辑器会通过准确的自动完成和即时错误反馈感谢你。

常见问题

直接在函数参数上对 props 进行类型注解。React.FC 会添加隐式的 children prop,无论你是否需要它,而直接对 props 进行类型注解更简洁。React 团队不推荐将 React.FC 作为默认模式。只有在特别需要其行为时才使用它,例如在处理期望它的旧代码库时。

对于内联处理程序,TypeScript 会从上下文自动推断事件类型。当你将处理程序提取到单独的函数时,使用显式类型,如 React.ChangeEvent 用于输入变化或 React.MouseEvent 用于点击。泛型参数指定元素类型,如 HTMLInputElement 或 HTMLButtonElement。

ReactNode 接受任何可渲染的内容,包括字符串、数字、null、undefined 和 JSX 元素。ReactElement 只接受 JSX 元素,排除原始值。大多数情况下使用 ReactNode。当你的组件特别需要 JSX children 并应该拒绝纯文本或数字时,使用 ReactElement。

不可以。从服务端组件传递到客户端组件的 props 必须可序列化。函数、类、Symbol 和其他不可序列化的值无法跨越服务端-客户端边界。在客户端组件内定义事件处理程序和回调,或使用服务端操作处理需要服务端处理的表单提交。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay