
先说个有意思的事今年我在 code review 里截图留档了一个新同事的 PR里面赫然写着const Card: React.FCCardProps () { ... }。倒不是说他写错了而是这套写法在前端社区已经被反复讨论了好几年到了 2026 年还全项目统一用React.FC多少有点“用着十年前的工具链写五年前的代码还觉得自己很规范”的意思。这篇文章我不准备再来一遍“React.FC 能不能用”的口水仗而是直接给你一套现在真正值得落地的 TypeScript 组件写法。会拆解React.FC的历史包袱、现阶段主流的 props 类型设计、泛型组件、组件通信的类型安全以及从旧写法迁移到新写法时的工程化步骤。适合所有用 React TypeScript 写业务或维护组件库的开发者尤其是团队里还在把React.FC当“规范”的人。1. React.FC 到底错在哪先把它拆干净1.1 它当年为什么能火起来2018 年前后React 16.8 刚带火 Hooks大量团队从 class 组件迁移到函数组件。那时候 TypeScript 对 JSX 的支持还没现在这么顺滑写一个函数组件如果不加类型标注props里全是隐式的any非常难受。React.FCFunctionComponent 的别名给了一个看起来很优雅的解法import React from react; interface CardProps { title: string; } const Card: React.FCCardProps ({ title }) { return div{title}/div; };这样写有几个当时很讨喜的特点函数入参自动帮你标好类型children会被隐式注入返回值也会被约束成ReactElement | null。对于刚从 class 组件切换过来、对 TypeScript 还没形成肌肉记忆的人来说React.FC就像“安全网”让你少想几件事。但安全网的代价是把你的一部分类型判断能力也一起收走了。等社区把函数组件的写法吃透之后回头再看React.FC身上的问题就一个个暴露出来了。1.2 现在回头看它的问题其实很具体第一隐式注入children是个陷阱。React.FCCardProps会让Card的 props 类型里“偷偷”多一个children?: ReactNode。假设你像上面那样定义CardProps本意是不想让Card接收任何子元素但用的时候Card我在哪/Card也不会报错类型系统在这里是失守的。更麻烦的是当你做 props 透传时这个多余的children还会造成跨组件赋值不匹配排查起来很头大。第二React.FC和泛型组件八字不合。React.FC是一个固定类型别名它接受一个泛型参数传入 props 类型但你没法基于它声明一个“组件本身是泛型”的场景。比如你写一个ListT组件让items和renderItem的类型跟随外部传入的T联动用React.FC根本表达不出来——你只能写const List: React.FCListPropsunknown然后把T硬编码成unknown内部还得不断做类型断言这基本等于放弃类型安全。第三defaultProps在React.FC上的类型推断一直有坑。函数组件的默认值正确做法是解构时直接赋初值function Button({ type button, children }: ButtonProps) { return button type{type}{children}/button; }用React.FC包裹后type字段在类型层面仍是“可选且有默认值”但拿到 props 的一端不会自动把type推断成必填很多时候还得手动?? button多写一层防御。说白了React.FC做的约束和真正想要的“props 显式化”之间存在一层隐式偏差。第四调试和性能上虽然差别不大但React.FC会给函数组件额外包一层类型标注部分场景下displayName、defaultProps的推导需要依赖FunctionComponent类型的静态属性兼容一旦你用了 memo 或 forwardRef这一层类型关系经常把自己绕进去。举个实际场景const MemoCard React.memoCardProps(Card);这行代码本身没问题但如果你一开始把Card定义成React.FCCardProps而CardProps里没有显式声明children在某些严格版本下React.memo的泛型推导会和FC自带的隐式children打架报一个让人摸不着头脑的类型错误。为了一个“省事”的写法反而要多写代码去绕弯这就本末倒置了。1.3 它还有没有存在的意义说实话到了 2026 年我找不到一个“必须用 React.FC”的硬场景。如果你在维护一个极老的代码库或者公司内部规定所有组件必须显式返回ReactElement | null那它是一个历史兼容选择不算错误但也别把它当新项目的默认值。新写的业务代码、组件库代码我建议一律不套React.FC。谁要是在你团队的新 PR 里看到React.FC直接拿我后面这几节的理由去对线就行。2. 现在的主流写法回归函数本身让类型显式化2.1 函数声明 独立 props 类型是最稳的底子我自己现在写组件的基线是这样import type { ReactNode } from react; type ButtonProps { variant: primary | secondary; children: ReactNode; onClick?: () void; }; function Button({ variant, children, onClick }: ButtonProps) { return ( button className{btn btn--${variant}} onClick{onClick} {children} /button ); }这版写法的核心逻辑就一句话把 props 当作普通函数的参数组件就是普通函数。ButtonProps是显式定义的children要不要、什么类型完全由你说了算入参解构直接得到类型不需要FC帮你做任何隐式注入函数的返回值交给 TypeScript 自己推导反正 JSX 的返回类型天然就是ReactElement | null推导不出来才叫奇怪。从团队协作的角度看这种写法的最大优势是“一个东西只有一个来源”。ButtonProps单独导出后别处要复用、要继承、要用工具类型派生都非常顺手。而React.FC的写法把组件类型和 props 类型绑死在了一个声明里想拆开用反而不方便。2.2 箭头函数到底能不能用关键看要不要泛型有些团队偏好const Xxx () {}的写法这没问题。但不建议写成const Button: React.FCProps () {}而是直接const Button ({ variant, children, onClick }: ButtonProps) { return button className{btn btn--${variant}}{children}/button; };不套FC之后箭头函数组件和函数声明组件在“普通 props 场景”下几乎没有区别。但一旦涉及泛型组件箭头函数就要多个心眼。在.tsx文件里T会被 JSX 解析器当成标签开头所以泛型箭头函数要写成T,const List T,({ items, renderItem }: ListPropsT) { return ul{items.map((item, index) renderItem(item, index))}/ul; };那个逗号不是手误是用来告诉 TypeScript“这里是泛型不是 JSX 标签”。很多小白第一次写泛型组件就卡在这一行。如果不想记这个偏门语法函数声明写法更省心因为function ListT(...)没有这个歧义。选择函数声明还是箭头函数我的标准是需要泛型优先函数声明只是简单组件两者都行队内统一个偏好就行。真正的重点是别再用FC把两者都框死。2.3 高阶组件和转发场景用什么类型当你写 HOC、写装饰器、或者封装forwardRef组件时直接裸函数已经不够用了这时候需要用 React 提供的基础类型工具。ComponentTypeP是“函数组件或类组件”的统一类型适合做 HOC 的入参类型import type { ComponentType } from react; type WithLoggingPropsT T { logMessage?: string }; function withLoggingT extends object( WrappedComponent: ComponentTypeT ) { return function LoggedComponent(props: WithLoggingPropsT) { console.log(props.logMessage ?? component rendered); return WrappedComponent {...props} /; }; }注意这里的T extends object是为了阻止别人传入string、number这种非对象类型当 props 基座。你不加这个约束TypeScript 不会主动拦你等到组件内部展开 props 时才报错那排查成本就上去了。至于forwardRefReact 19 已经在标准 props 里支持ref作为普通属性传入不需要再包一层forwardRef。但如果你还在维护 React 18 及以下的代码旧式写法里ForwardRefRenderFunction比React.FC更准确import { forwardRef } from react; import type { ForwardRefRenderFunction } from react; interface InputProps { label: string; } const InputInner: ForwardRefRenderFunctionHTMLInputElement, InputProps ( { label }, ref ) input ref{ref} aria-label{label} /; export const Input forwardRef(InputInner);这套写法的关键是“类型意图分离”第一个泛型参数是 ref 指向的 DOM 元素类型第二个才是组件 props 类型。用React.FC写 forwardRef 的话ref 类型和 props 类型会搅在一起推导经常失真。3. 进阶模式泛型组件、派生 Props 与通信类型安全3.1 泛型组件是 2026 年组件设计的及格线现在做 UI 组件库泛型组件几乎成了标配。最典型的场景是列表和选择器import type { ReactNode } from react; type ListPropsT { items: T[]; renderItem: (item: T, index: number) ReactNode; keyExtractor: (item: T, index: number) string | number; }; function ListT({ items, renderItem, keyExtractor }: ListPropsT) { return ( ul {items.map((item, index) ( li key{keyExtractor(item, index)}{renderItem(item, index)}/li ))} /ul ); }使用的时候TypeScript 会根据items和renderItem自动推断T如果renderItem里写了一个不存在的字段立即报错。这比用Listany然后靠运行时炸锅要可靠太多。泛型组件再往后走会遇到“多泛型联动”的需求。比如一个SelectT, K组件value的类型由T决定onChange回调接收的类型由K决定type SelectPropsT, K { options: T[]; value: K; getValue: (item: T) K; onChange: (value: K) void; }; function SelectT, K({ options, value, getValue, onChange }: SelectPropsT, K) { return ( select value{String(value)} onChange{(e) { const matched options.find((opt) String(getValue(opt)) e.target.value); if (matched) onChange(getValue(matched)); }} {options.map((opt) ( option key{String(getValue(opt))} value{String(getValue(opt))} {JSON.stringify(opt)} /option ))} /select ); }这类组件用React.FC是写不出来的。组件库越做越深泛型几乎是绕不开的工具。3.2 派生 Props别把 HTML 原生属性重新写一遍业务组件一个常见的需求是“包装一个 button / input同时支持原生属性”。过去很多人会手动把onClick、disabled、className一个个复制到自己的 props 类型里不仅累还经常漏。正确做法是用ComponentProps或ComponentPropsWithoutRef派生import type { ComponentPropsWithoutRef } from react; type ButtonProps ComponentPropsWithoutRefbutton { variant?: primary | ghost | danger; loading?: boolean; }; function Button({ variant primary, loading false, disabled, children, ...rest }: ButtonProps) { return ( button className{btn btn--${variant}} disabled{disabled || loading} {...rest} {loading ? 加载中... : children} /button ); }这样Button天然支持type、aria-label、>type ButtonProps OmitComponentPropsWithoutRefbutton, type { type?: button | submit | reset; variant?: primary | ghost; }; function Button({ type button, ...rest }: ButtonProps) { return button type{type} {...rest} /; }注意Omit的顺序先剔除再补上才能精准替换。反过来如果先再Omit会把自己追加的type也删掉这就翻车了。3.3 组件通信的类型安全父传子、子传父、Context 三种都要有业务开发中组件通信绕不开“父传子、子传父、跨层级 Context”三种。父传子最简单就是普通的 props 类型定义。子传父本质上就是回调函数关键是回调的参数类型要定义清楚。我建议把“事件动作”和“携带数据”拆开避免回调里塞一个笼统的anytype SortAction | { type: sort; field: name | age; direction: asc | desc } | { type: filter; keyword: string }; type ToolbarProps { onAction: (action: SortAction) void; }; function Toolbar({ onAction }: ToolbarProps) { return ( div button onClick{() onAction({ type: sort, field: name, direction: asc })} 按名称排序 /button button onClick{() onAction({ type: filter, keyword: })} 清除筛选 /button /div ); }这种可辨识联合discriminated union的好处是父组件拿到action后用一个switch (action.type)就能让 TypeScript 精确收窄每一分支的数据类型。比直接传(field: string, direction: string) void这种松散回调安全得多。Context 场景我推荐把 context 的默认值设计成“显式可空”而不是“给个空对象然后到处断言”import { createContext, useContext } from react; interface ThemeContextValue { theme: light | dark; toggleTheme: () void; } const ThemeContext createContextThemeContextValue | null(null); export function useThemeContext() { const ctx useContext(ThemeContext); if (!ctx) { throw new Error(useThemeContext 必须在 ThemeContext.Provider 内使用); } return ctx; }用一个自定义 hook 包一层而不是让组件直接useContext(ThemeContext)然后手动判空这是我在项目里踩过坑后定的规矩。否则每个消费组件都要写一遍判空逻辑总有人会偷懒不判等到undefined出现时又是一个诡异的运行时错误。3.4 React 19 与 TypeScript 5.x 带来的新变化到了 2026 年React 19 已经是很常见的基线版本了。它最大的变化之一是ref可以直接作为普通 prop 传递forwardRef不再必需。类型上对应的写法更清爽type InputProps { label: string; ref?: React.RefHTMLInputElement; }; function Input({ label, ref }: InputProps) { return input ref{ref} aria-label{label} /; }如果你在用 React 19新组件就别再套forwardRef了直接用上面的方式即可。老组件迁移也不难把forwardRef包的那层去掉ref声明移到 props 里。TypeScript 这方面5.x 系列的const类型参数、更快的增量构建对大型前端项目帮助很大。有一个编译器选项我建议新项目直接开verbatimModuleSyntax: true。它会强制你使用import type导入纯类型从根上避免类型被编译进产物导致的循环引用问题。开了这个选项上面我写的import type { ReactNode }这种风格就是必须的了而不是可选的“推荐写法”。另外一个值得留意的选项是erasableSyntaxOnly: trueTypeScript 5.8 引入的它限制你使用那些需要运行时保留的语法比如 enum、namespace、类参数属性因为 React 组件类型大多只是“可擦除”的结构化类型这套约束能保证类型系统和编译产物解耦。如果你在维护组件库这个选项可以让产物体积更可控类型也更干净。4. 工程化落地怎么从 React.FC 迁到新写法还不翻车4.1 用 ESLint 把门槛卡住代码规范靠 code review 人肉盯迟早漏。强烈建议在 ESLint 配置里把“禁止使用 React.FC”自动化。你可以直接用typescript-eslint配合no-restricted-syntax或者找一个现成的规则插件。我项目里用的自定义规则类似这样// eslint.config.js 片段 export default [ { files: [**/*.tsx], rules: { no-restricted-syntax: [ error, { selector: TSTypeReference[typeName.nameFC], message: 请勿使用 React.FC改用普通函数声明 显式 props 类型 } ] } } ]另外两个推荐开的规则typescript-eslint/consistent-type-definitions设置为type强制团队统一用type而不是interface定义 props减少两种写法混用带来的认知负担这条看团队偏好至少统一一种react/display-name保留但对“不写 React.FC 的普通函数组件”它通常不误报不用额外关。还有一点容易被忽略typescript-eslint/consistent-type-imports开了之后所有纯类型导入会被强制写成import type { ... }。这能有效防止你一边标榜“不用 FC”一边在文件顶部import React, { FC } from react。4.2 迁移旧组件的实操步骤我建议按下面五步走别想着一次性全部替换风险太大。先把组件的 props 类型补完整。React.FCCardProps里面的CardProps如果之前依赖隐式children先显式加上children?: ReactNode避免删掉 FC 后出现一堆 children 相关的类型报错。把const Xxx: React.FCProps ({ ... }) {}改成const Xxx ({ ... }: Props) {}。这一步只去类型标注不动逻辑配合 ESLint 规则后每个文件改动极小review 成本低。如果遇到泛型组件或复杂 HOC确认原先React.FC是否参与了对泛型的错误收窄改完函数声明后重新检查useState、useMemo的推断是否正常。跑一遍全量tsc --noEmit把报错集中收集大部分是children缺失、defaultProps类型不匹配、React.memo泛型冲突这三类。逐个修复即可。最后用react-component-remove-fc之类的 codemod或自己写的正则做一次全局扫描确认没有漏网之鱼。注意 codemod 只能处理简单的React.FCProps模式遇到React.FCProps { defaultProps... }这种组合还是得手改。我实践下来一个两三百个组件的项目按这个顺序分批改一周内能完成前提是测试覆盖不要太差。4.3 常见报错排查速查表报错现象常见原因解决办法Property children does not exist on type Props之前依赖React.FC隐式注入 children在 props 类型里显式声明children?: ReactNodeType Element is not assignable to type FCProps手写了React.FC却没和函数签名对上去掉React.FC标注保留 props 解构类型Expected 2 type arguments, but got 1React.memo在部分版本里泛型参数数量不同不用React.memoProps(...)直接用memo(Component)让 TS 推断T,语法处报 JSX 错误在.tsx里写泛型箭头函数少了逗号改为T,或改用function声明ButtonProps is missing the following properties from type ButtonPropsprops 里存在可选字段但使用处必须传检查extends或交叉类型里是否覆盖成了必填Cannot find namespace React新版 React 类型导入方式变了使用import type { ReactNode } from react显式导入别依赖 UMD 全局这张表我都是拿实际报错堆出来的你照着排查基本够用。如果还有没覆盖到的优先看tsc的完整报错链大部分 TypeScript 问题都能从“第一个报错”往下推导别被后续的连锁报错带偏。4.4 团队落地时的一点心得最后给个很实际的建议不要搞“一刀切”的强制重构。我见过有团队把“禁 React.FC”写进规范后要求一晚上全量替换结果第二天一半组件在冒类型错误新同事还以为是自己的问题。比较稳的做法是分两步走存量代码只加 ESLint 规则但不自动改新代码必须按新规范写然后每周挑一批低风险组件做迁移把“技术债”拆成一个个可回滚的小任务。从长期维护角度看后面我也建议把这些新写法沉淀成团队的组件模板。创建新组件时直接引用模板比 review 时反复提醒有效得多。等模板用顺手后团队里自然不会再有人想起来React.FC这回事。这比任何规范文档都管用。我自己的体会是React 组件的 TypeScript 写法本质上是“在类型上多花一点心思在排查上省一大段时间”。React.FC的问题不在于它本身是错的而在于它用一层隐式魔法把类型问题推迟到了使用端。换成显式 props 类型、泛型组件、可辨识联合之后大部分类型问题会在写代码的当下就被 IDE 拦住。2019 年大家都在找捷径2026 年该把路走扎实了。