TSX 与类型化前端组件的兴起
涵盖 props 类型标注、事件处理、children 模式及 React 19 中服务端与客户端组件划分,助力编写类型完备的 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
}
Discover how at OpenReplay.com.
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 进行类型注解。让类型推断发挥作用。你的编辑器会通过准确的自动完成和即时错误反馈感谢你。
常见问题
我应该使用 React.FC 还是直接在函数参数上对 props 进行类型注解?
直接在函数参数上对 props 进行类型注解。React.FC 会添加隐式的 children prop,无论你是否需要它,而直接对 props 进行类型注解更简洁。React 团队不推荐将 React.FC 作为默认模式。只有在特别需要其行为时才使用它,例如在处理期望它的旧代码库时。
如何在 React 中使用 TypeScript 对事件处理程序进行类型注解?
对于内联处理程序,TypeScript 会从上下文自动推断事件类型。当你将处理程序提取到单独的函数时,使用显式类型,如 React.ChangeEvent 用于输入变化或 React.MouseEvent 用于点击。泛型参数指定元素类型,如 HTMLInputElement 或 HTMLButtonElement。
ReactNode 和 ReactElement 在类型化 children 时有什么区别?
ReactNode 接受任何可渲染的内容,包括字符串、数字、null、undefined 和 JSX 元素。ReactElement 只接受 JSX 元素,排除原始值。大多数情况下使用 ReactNode。当你的组件特别需要 JSX children 并应该拒绝纯文本或数字时,使用 ReactElement。
在 React 19 中,我可以从服务端组件向客户端组件传递函数作为 props 吗?
不可以。从服务端组件传递到客户端组件的 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.
Star on GitHub12k