TypeScript里React.FC和直接写函数有什么区别?

作者:异步漫步时间:2026-11-12 23:55:56浏览:586

深入对比TypeScript中React.FC与直接声明函数的区别,剖析类型推导、children属性演变、泛型组件支持以及现代React最佳实践。

React 技术指南 适用版本:React 17 / 18 / 19 阅读时长:约 6 分钟
TypeScript里React.FC和直接写函数有什么区别?
图 0:React.FC 与直接声明函数的模式比对总览
在 TypeScript 中编写 React 组件时,React.FC(全称 React.FunctionComponent)与“直接写普通函数”的核心区别在于:React.FC 是一个显式定义函数外层签名的泛型接口,它同时约束了组件的形参和返回值类型,并挂载了静态元属性;而直接写函数则是利用 TypeScript 原生函数的类型推导机制,只单独约束 props 参数,组件返回值由 JSX 语法自然推断。在过去,React.FC 会默认注入隐式的 children 属性,随着 React 18 对该隐患的移除,直接写函数在泛型支持、异步组件兼容以及语法简洁度上的综合优势更加突出,已成为现代 React 开发的普遍共识。

一、两种写法的基本形态对比

在开始深入底层类型机制之前,先直观查看两种写法在代码中的实际表达方式。假设我们正在构建一个展示用户信息徽章的组件 UserBadge:

1. 使用 React.FC 的写法

import React from 'react';

interface UserBadgeProps {{
  name: string;
  role?: string;
}

// 将类型直接标注在变量常量上
export const UserBadge: React.FC<UserBadgeProps> = ({{ name, role = 'User' }}) => {
  return (
    <div className="user-badge">
      <span>{name}</span>
      <small>({role})</small>
    </div>
  );
};

2. 直接写函数的写法(推荐)

import React from 'react';

interface UserBadgeProps {{
  name: string;
  role?: string;
}

// 直接在形参上标注 Props 类型,或使用标准 function 关键字
export function UserBadge({{ name, role = 'User' }}: UserBadgeProps) {{
  return (
    <div className="user-badge">
      <span>{name}</span>
      <small>({role})</small>
    </div>
  );
}

// 同样也可以使用常规的箭头函数格式
export const UserBadgeArrow = ({{ name, role }}: UserBadgeProps) => (
  <div className="user-badge">{name}</div>
);

表面看来,两者只是把类型写在冒号左侧(变量)还是右侧(参数)的区别。但正是在这一微小的位置转移背后,引发了类型推断、语言特性支持和代码维护性的一系列差异。

TypeScript里React.FC和直接写函数有什么区别?
图 1:React.FC 与直接声明函数的底层类型结构比对

二、核心区别之一:children 属性的隐式注入与演进

这是 React.FC 在社区中长期遭受争议、也是促使大量团队废弃它的最关键原因。

1. React 17 及早期版本的隐式陷阱

在 React 17 及之前的 @types/react 声明文件中,React.FC 的底层定义是类似如下的接口:

// React 17 早期 @types/react 源码片段
type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {{
  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
  // ...
}}

注意其中的 PropsWithChildren<P>。这意味着只要你给组件标注了 React.FC,该组件的 props 就会自动拥有 children?: ReactNode 属性,哪怕你在自己的 Props 接口中压根没有定义 children!

这就导致了类型漏洞:如果某个组件在设计上是一个自闭合的纯展示卡片(不接收任何子节点),在调用方错误地传入子元素时,TypeScript 编译器不会给出任何报错:

// 开发者误传了 children,但 TS 保持沉默,页面也不会渲染此内容,排查极其困难
<UserBadge name="Alice">
  <span>这里传的内容永远不会被渲染!</span>
</UserBadge>

2. React 18+ 的修正与现代规范处理

从 React 18 的 @types/react 开始,官方团队采纳社区意见,正式将 children 从 React.FC 的默认签名中移除。现在的 React.FC 不再自带 children,如果需要,必须显式定义。

TypeScript里React.FC和直接写函数有什么区别?
图 2:children 属性机制演变与排查处理步骤

而在“直接写函数”的范式中,这种问题从第一天起就不存在:组件接收什么,接口就写什么。这种行为完全符合代码的可预测性原则。若确实需要接收子节点,推荐以下两种直观明了的写法:

// 推荐方式一:在专属接口中明确声明,语义最清晰
interface ContainerProps {{
  title: string;
  children: React.ReactNode; // 必须传入子节点
}

// 推荐方式二:借助 React 提供的 PropsWithChildren 辅助工具泛型
import {{ PropsWithChildren }} from 'react';

type ContainerProps = PropsWithChildren<{
  title: string;
}>;

三、核心区别之二:泛型组件(Generic Components)的支持难度

在前端业务中,表格(Table)、选择器(Select)、下拉菜单(Dropdown)等高阶展示组件通常需要接收泛型数据。在泛型组件的实现上,普通函数和 React.FC 表现出了决定性的差距。

1. 直接写函数天然拥抱泛型

因为直接写函数遵循标准 TypeScript 函数规则,定义泛型组件就像写普通泛型函数一样自然流畅:

interface TableProps<T> {{
  items: T[];
  renderRow: (item: T) => React.ReactNode;
}

// 使用 function 关键字定义泛型组件
export function Table<T>({ items, renderRow }: TableProps<T>) {{
  return (
    <table>
      <tbody>
        {items.map((item, idx) => (
          <tr key={idx}>{renderRow(item)}</tr>
        ))}
      </tbody>
    </table>
  );
}

// 使用时,TypeScript 能根据传入的 items 自动推导出 T 的实际类型!
<Table items={[{ id: 1, name: 'Item A' }]} renderRow={(item) => <td>{{item.name}}</td>} />

2. React.FC 在泛型声明上的语法困境

如果你尝试用 React.FC 来写同一个泛型组件,立刻就会遭遇 TypeScript 编译器的红线:

// 语法报错:TS 无法识别 T
const Table: React.FC<TableProps<T>> = (props) => { ... }; 
// Error: Cannot find name 'T'.

原因在于:常量声明 const Table 本身无法在左侧接受泛型形参。如果非要用 React.FC 实现泛型组件,开发者不得不写出极度晦涩的高阶包装或强制类型断言(如 as React.FC),这不仅丢失了类型推导的灵活性,也彻底背离了书写整洁代码的初衷。

TypeScript里React.FC和直接写函数有什么区别?
图 3:泛型组件编写体验与推导对比

四、核心区别之三:返回值约束与现代异步组件(RSC)

在传统的客户端渲染中,组件通常返回 JSX 节点或 null。但在现代 React 架构中,返回值的要求发生了演进:

  • React.FC 的强制约束:它要求函数的返回值必须严格契合其内部定义的返回类型(早期为 ReactElement | null,较新版本调整为 ReactNode)。如果你意外返回了一个未在当前联合类型中的数据,或者在返回类型匹配上有微弱差异,就会报错。
  • 现代服务端组件(React Server Components):在 Next.js App Router 以及 React 19 的服务端组件中,组件可以直接声明为 async 函数并执行异步数据获取:
    // 服务端异步组件:返回 Promise<JSX.Element>
    export async function UserProfile({ userId }: { userId: string }}) {{
      const user = await fetchUser(userId);
      return <div>{user.name}</div>;
    }}
    如果你给这个异步函数标注 React.FC,TypeScript 会立刻报错,因为 React.FC 的返回类型签名只允许同步节点,无法接受返回 Promise 的函数结构。

五、核心区别之四:静态属性与代码风格

早期 React.FC 存在的一个所谓“优势”在于它在类型上内置了静态属性声明,例如 defaultProps、displayName、propTypes 等:

const Button: React.FC<Props> = ...;
Button.displayName = 'CustomButton'; // 类型声明中已允许挂载
Button.defaultProps = { ... };      // 过去支持 defaultProps 静态定义

然而在现代 React 规范中:

  1. defaultProps 在函数组件中已被废弃:React 官方早已明确宣布在函数组件中废弃 defaultProps,全面推荐使用 ES6 原生解构默认值(如 function Button({ size = 'medium' }: Props)),更简单且完全符合语言规范。
  2. displayName 可以直接赋值:对于直接写的函数组件,如果需要调试名,直接赋值 Button.displayName = 'CustomButton' 同样能够正常工作,无需依赖 React.FC。
  3. 代码精简度:使用 React.FC 必须额外书写 React.FC<...>,在每个组件文件中平添了冗余字符。直接写函数更清爽直观。

六、选型决策指南与现代最佳实践

综合以上核心差异,我们可以将各种开发场景归纳为清晰的选择指引:

TypeScript里React.FC和直接写函数有什么区别?
图 4:现代 React 组件声明方式技术选型与总结决策树
对比维度 React.FC<Props> 直接写函数 (推荐)
语法形式 const Comp: React.FC<P> = ... function Comp(props: P) { ... }
children 处理 React 17 前隐式注入;React 18 后需手动定义 始终显式定义,所见即所得
泛型组件支持 极其繁琐,需复杂断言或高阶包装 原生完美支持 <T> 声明与推导
服务端异步组件 (RSC) 不支持(返回类型冲突) 直接支持 async function
默认值处理 曾绑定 defaultProps(现已废弃) 标准 ES6 参数默认值解构
官方与社区态度 不再主推,逐步淡出主流实践 React 官方文档与主流社区推荐标准
💡 现代项目重构与编写建议

1. 新项目统一采用直接写函数:团队新起项目建议在代码规范(如 ESLint / Prettier)中统一推荐 function Component(props: Props) 或箭头函数参数注解,彻底避免泛型与未来异步特性的兼容问题。

2. 存量项目无需盲目全量重写:如果现有代码库基于 React 18+ 且全量使用了 React.FC,由于隐式 children 已经被修复,运行期和构建期不会有严重安全漏洞。可以在新增组件和通用公共组件中采用新规范,平滑演进。

总结

React.FC 是 React 与 TypeScript 结合早期的产物,旨在为开发者提供一个一站式的函数组件类型接口;但随着 TypeScript 原生能力的增强以及 React 自身设计理念向“纯 JavaScript 函数”的回归,React.FC 的许多包装不仅显得多余,反而在泛型、异步组件和语法简洁性上带来了额外阻碍。

记住核心结论:在绝大多数现代 React 项目中,直接为函数参数标注 Props 类型,让返回值由 TypeScript 自然推导,是最清爽、扩展性最好、也是最符合官方演进方向的选择。

作者最新文章
相关文章
更多