React 和 TypeScript 中 props children 背后隐藏着什么?
\n\n如果你曾经使用 TypeScript 编写过 React 组件,那么你很可能遇到过这个问题:"props children 的数据类型是什么?"。这个问题是 Stack Overflow 上最热门的问题之一,这并非没有原因。乍一看,答案似乎很简单:ReactNode。但正如俗话所说,细节是魔鬼。
在本文中,我们不会仅仅给出一个干巴巴的答案。我们将深入探讨 children 所有可能的类型、它们的区别、最佳实践以及潜在的陷阱。你将了解何时使用 ReactNode,何时使用 ReactElement,以及何时使用更具体的类型。我们开始吧!
为什么这个问题如此热门?
\n\n问题在于,children 是 React 中的一个特殊 prop。它可以是任何东西:
- \n
- 字符串:
"你好,世界!"\n - 数字:
{42}\n - JSX 元素:
<div>...</div>\n - 元素数组:
[<span/>, <span/>]\n - 函数(render prop):
{() => <div/>}\n - 甚至是
null、undefined或boolean\n
由于这种灵活性,新手经常写成 children: any,这完全抵消了 TypeScript 带来的好处。让我们来探讨正确的方法。
children 的主要类型
\n\n1. ReactNode —— 全能选手
\n\n这是 90% 情况下 children 最常用且最正确的类型。它包含了 React 可以渲染的所有内容。
import { ReactNode } from 'react';\n\ninterface CardProps {\n title: string;\n children: ReactNode;\n}\n\nfunction Card({ title, children }: CardProps) {\n return (\n <div className="card">\n <h2>{title}</h2>\n <div className="card-content">\n {children}\n </div>\n </div>\n );\n}\n\n// 使用方式:\n<Card title="我的卡片">\n <p>这段文本将被渲染</p>\n {123} {/* 数字也可以 */}\n {null} {/* null 也可以 */}\n</Card>\n\nReactNode 包含什么? 实际上它是以下类型的联合:
\n- \n
ReactElement—— JSX 元素 \n string和number—— 原始类型 \n boolean、null、undefined—— 渲染时会被忽略 \n ReactNode[]—— 上述类型的数组 \n ReactPortal—— 传送门 \n
2. ReactElement —— 严格模式
\n\n如果你需要 children 只能是 JSX 元素(不能是字符串或数字),请使用 ReactElement。
import { ReactElement } from 'react';\n\ninterface StrictCardProps {\n children: ReactElement; // 仅限一个 JSX 元素\n}\n\nfunction StrictCard({ children }: StrictCardProps) {\n return <div className="strict-card">{children}</div>;\n}\n\n// 可行:\n<StrictCard><span>仅限元素</span></StrictCard>\n\n// TypeScript 报错:\n<StrictCard>文本</StrictCard> // ❌ 字符串不符合要求\n\n重要提示: ReactElement 要求恰好有一个子元素。如果传递两个元素而没有包裹,TypeScript 也会报错。
3. JSX.Element —— ReactElement 的别名
\n\nJSX.Element 类型实际上就是 ReactElement<any, any>。它不如 ReactNode 灵活,通常不推荐用于 children,除非你在编写库。
// 本质上,它们是相同的:\ntype A = JSX.Element; // ReactElement<any, any>\ntype B = ReactElement; // Re