首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >2026了,你还在用 React.FC 写组件?这些 TypeScript 模式该更新了

2026了,你还在用 React.FC 写组件?这些 TypeScript 模式该更新了

作者头像
前端达人
发布2026-03-12 13:06:42
发布2026-03-12 13:06:42
30
举报
文章被收录于专栏:前端达人前端达人

🎊 大年初一特刊 · 前端达人新春献礼

前几天帮朋友 Code Review,看到一段 2024 年写的 React 代码:

代码语言:javascript
复制
const UserCard: React.FC<{ name: string }> = ({ name, children }) => {
  return <div>{name}{children}</div>
}

第一反应是:这代码没问题啊,能跑。但仔细一想——children 是哪来的?React.FC 自带了隐式的 children 类型,你以为写了个只接收 name 的组件,实际上任何人都可以往里塞任何内容,TypeScript 不会报一个字的错。

这就好比你家门上挂了把锁,但有人发现侧面开了一扇暗门,进出自由,锁形同虚设。

这就是 TypeScript 用了等于没用的典型案例。

2026年,这些问题早有更好的解法。今天这篇文章,我们就系统梳理一遍 React + TypeScript 在当下真正值得用的模式——不是拿来炫技的,而是每天都会用到、用了就回不去的那种。

一、先搞清楚:TypeScript 为什么配和 React 搭档?

打个比方:写 JavaScript 的 React 项目,就像在没有地图的城市里开车。你大概知道方向,但每次拐弯都要猜一猜——这个 props 到底传了什么?这个函数的返回值是啥类型?出了 bug 才发现原来走错了路。

TypeScript 给你装上了导航。它在你写代码的时候就告诉你:这里不对,那里缺了什么,向右转有一条更快的路。

2025年,JS 生态里的 TypeScript 采用率已经到了 78%(State of JS 数据),大厂项目几乎清一色 TypeScript。字节跳动内部许多 React 项目,Ant Design、arco-design 这些组件库,全部基于 TypeScript 构建。你会 TypeScript,不只是加分项,是基础门槛。

好,背景说完,直接进正题。

二、新 JSX Transform:从此不用每个文件写 import React

以前为什么要 import React?

React 17 之前,写 JSX 其实是在写这个:

代码语言:javascript
复制
// 你写的
const el = <div>hello</div>

// 编译器翻译成
const el = React.createElement('div', null, 'hello')

所以必须引入 React,不然 React.createElement 找不到,报错。

现在不需要了

React 17+ 引入了新的 JSX Transform,编译器会自动从 react/jsx-runtime 引入需要的函数,不再依赖全局的 React 变量。

代码语言:javascript
复制
// ✅ 2026 写法:不需要 import React
interface ButtonProps {
  label: string;
  onClick: () =>void;
  variant?: 'primary' | 'secondary' | 'danger';
}

exportconst Button = ({ 
  label, 
  onClick, 
  variant = 'primary'
}: ButtonProps) => {
return (
    <button 
      onClick={onClick}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
};

但这需要你的 tsconfig.json 配置正确:

代码语言:javascript
复制
{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

💡 很多同学升级了 React 版本,但忘记更新 tsconfig,导致项目里还是一堆多余的 import React,虽然不影响运行,但是冗余代码,代码评审会被挑。

三、告别 React.FC:显式声明 props 才是正道

为什么要放弃 React.FC

React.FC 这个类型有两个主要问题:

问题一:隐式包含 children(React 18 之前)

代码语言:javascript
复制
// ❌ 旧写法:React.FC 在 React 17 及更早版本中隐式包含 children
const Card: React.FC<{ title: string }> = ({ title, children }) => {
  // children 哪来的?React.FC 帮你偷偷加上了
  return <div>{title}{children}</div>
}

就好比你订了一份外卖,只点了宫保鸡丁,结果送来的时候多了一碗不知道啥口味的汤,你没点,但它来了,你也不知道该怎么处理它。

问题二:表达能力不够强

显式声明 props interface,你能更精确地控制每个 prop 的类型和是否必填,代码意图一目了然。

代码语言:javascript
复制
// ✅ 2026 推荐写法:完全显式
interface CardProps {
  title: string;
  subtitle?: string;
  children?: React.ReactNode;  // 你自己决定要不要接收 children
  onClose?: () =>void;
}

const Card = ({ title, subtitle, children, onClose }: CardProps) => {
return (
    <div className="card">
      <div className="card-header">
        <h3>{title}</h3>
        {subtitle && <p>{subtitle}</p>}
        {onClose && <button onClick={onClose}>×</button>}
      </div>
      {children && <div className="card-body">{children}</div>}
    </div>
  );
};

这样每一个 prop 的存在都有明确的理由,读代码的人(包括三个月后的你自己)一看就懂。

四、泛型组件:一次编写,无数复用

场景:你需要一个通用列表组件

你们团队做电商项目,既要展示商品列表,又要展示订单列表,还要展示用户列表。写三个几乎一样的组件?太蠢。写一个 any 类型的?TypeScript 等于没用。

正确答案:泛型组件。

代码语言:javascript
复制
用户需求
   |
   v
┌─────────────────────────────────────┐
│         <List<T>> 泛型组件           │
│                                     │
│  items: T[]  ──→  遍历每一项         │
│  renderItem: (item: T) => JSX       │
│  keyExtractor: (item: T) => string  │
└─────────────────────────────────────┘
   |            |              |
   v            v              v
商品列表      订单列表       用户列表
Product[]   Order[]        User[]
代码语言:javascript
复制
// 定义泛型列表组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) =>string;
  emptyText?: string;
  loading?: boolean;
}

const List = <T,>({ 
  items, 
  renderItem, 
  keyExtractor,
  emptyText = '暂无数据',
  loading = false
}: ListProps<T>) => {
if (loading) return <div className="loading">加载中...</div>;
  if (items.length === 0) return <div className="empty">{emptyText}</div>;

return (
    <ul className="list">
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
};

实际使用时 TypeScript 完全自动推断类型:

代码语言:javascript
复制
interface Product {
  id: string;
  name: string;
  price: number;
}

// TypeScript 知道 product 是 Product 类型,直接有完整提示
<List
  items={products}
  renderItem={(product) => (
    <span>{product.name} - ¥{product.price}</span>
  )}
  keyExtractor={(product) => product.id}
  emptyText="暂无商品"
/>

这就是泛型的魔法:写一次,TypeScript 在每次使用时自动帮你核查类型,无需重复声明。

五、可辨识联合类型:告别 undefined 地狱

一个你每天都会遇到的问题

请求数据时,组件有几种状态:加载中、成功、失败。

很多人这样写:

代码语言:javascript
复制
// ❌ 容易出问题的写法
interface State {
  loading: boolean;
  data?: User[];
  error?: string;
}

问题来了:loading: false + data: undefined + error: undefined ——这是初始状态还是请求失败了?loading: false + data: [...] + error: "网络错误" ——同时有数据又有错误,这是什么情况?

这些"非法状态"TypeScript 完全无法拦截。

可辨识联合类型:让非法状态不可能存在

代码语言:javascript
复制
            ┌── { status: 'idle' }
            │
状态只能是 ──┼── { status: 'loading' }
            │
            ├── { status: 'success'; data: User[] }
            │
            └── { status: 'error'; error: string }

每种状态下 TypeScript 精确知道有哪些字段
非法状态在类型层面根本无法构造
代码语言:javascript
复制
// ✅ 可辨识联合类型
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; retryable: boolean };

const UserList = () => {
const [state, setState] = useState<RequestState<User[]>>({ 
    status: 'idle'
  });

// TypeScript 的"侦探推理":知道了 status 就知道有哪些字段
switch (state.status) {
    case'idle':
      return <button onClick={fetchUsers}>加载用户</button>;
    
    case 'loading':
      return <Skeleton count={5} />;
    
    case'success':
      // TypeScript 100% 确定 state.data 存在且是 User[]
      return <ul>{state.data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
    
    case'error':
      return (
        <div>
          <p>出错了:{state.error}</p>
          {state.retryable && <button onClick={fetchUsers}>重试</button>}
        </div>
      );
  }
};

这种写法在大型项目里价值非凡。阿里、字节的前端团队大量使用这个模式处理异步状态,原因很简单:它从根本上杜绝了"数据和状态对不上"这类 bug。

六、自定义 Hook 的类型:返回元组别踩坑

场景:封装 localStorage 读写

代码语言:javascript
复制
// 返回元组,用 as const 锁定类型
const useLocalStorage = <T,>(key: string, initialValue: T) => {
const [value, setValue] = useState<T>(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? (JSON.parse(stored) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setStoredValue = (newValue: T | ((prev: T) => T)) => {
    try {
      const valueToStore = newValue instanceofFunction
        ? newValue(value) 
        : newValue;
      setValue(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('写入 localStorage 失败:', error);
    }
  };

const removeValue = () => {
    localStorage.removeItem(key);
    setValue(initialValue);
  };

// as const 关键!让 TypeScript 把返回值推断为元组而不是数组
return [value, setStoredValue, removeValue] asconst;
};

为什么必须加 as const

代码语言:javascript
复制
// 不加 as const:TypeScript 认为返回的是数组
// 推断类型:(T | ((prev: T) => T) | (() => void))[]
// 调用时:setValue 可能被推断成 (() => void) 类型,报错

// 加了 as const:TypeScript 精确推断为元组
// 推断类型:readonly [T, (newValue: T | ...) => void, () => void]
// 调用时:每个位置的类型都精确,IDE 提示完美

const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');
// theme: string ✅
// setTheme: (newValue: string | ((prev: string) => string)) => void ✅
// removeTheme: () => void ✅

七、开启严格模式:让 TypeScript 真正帮你抓 Bug

tsconfig.json 这几个选项,开了就回不去

代码语言:javascript
复制
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true
  }
}

用一个比喻来解释这些配置的作用:

代码语言:javascript
复制
strict: true
= 开启安全驾驶模式(强制系安全带)
  ↓
  启用 strictNullChecks, strictFunctionTypes 等全套检查

noUncheckedIndexedAccess: true
= 给数组访问装上安全气囊
  ↓
  const arr = [1, 2, 3];
  const item = arr[10];  // 类型变为 number | undefined,必须判断
  item.toFixed();        // ❌ 报错!可能是 undefined

exactOptionalPropertyTypes: true
= 区分"属性不存在"和"属性值为 undefined"
  ↓
  interface Config { timeout?: number }
  const c: Config = { timeout: undefined }; // ❌ 报错!
  const c: Config = {};                     // ✅ 正确

开启严格模式的代价: 可能出现一批新的类型错误,但这些都是真实存在的潜在 bug,TypeScript 替你提前发现了。

八、一张图看清完整的 TypeScript + React 最佳实践

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────┐
│                 2026 TypeScript + React 架构图               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  tsconfig.json                                             │
│  ┌──────────────────────────────────────┐                  │
│  │ "jsx": "react-jsx"  ← 新 JSX 转换    │                  │
│  │ "strict": true      ← 严格模式        │                  │
│  │ "noUncheckedIndexedAccess": true     │                  │
│  └──────────────────────────────────────┘                  │
│                          ↓                                  │
│  组件层                                                     │
│  ┌─────────────────────────────────────┐                   │
│  │  interface Props { ... }  ← 显式声明 │                   │
│  │  const Comp = (props: Props) => {}  │                   │
│  │  ❌ 不用 React.FC                   │                   │
│  │  ✅ 需要 children 显式写 ReactNode   │                   │
│  └─────────────────────────────────────┘                   │
│                          ↓                                  │
│  状态管理层                                                  │
│  ┌─────────────────────────────────────┐                   │
│  │  可辨识联合类型管理异步状态          │                   │
│  │  type State =                       │                   │
│  │    | { status: 'idle' }             │                   │
│  │    | { status: 'loading' }          │                   │
│  │    | { status: 'success'; data: T } │                   │
│  │    | { status: 'error'; msg: str }  │                   │
│  └─────────────────────────────────────┘                   │
│                          ↓                                  │
│  复用层                                                     │
│  ┌─────────────────────────────────────┐                   │
│  │  泛型组件 <List<T>>                  │                   │
│  │  自定义 Hook + as const 元组         │                   │
│  │  Zod 做运行时校验                    │                   │
│  └─────────────────────────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

九、快速落地检查清单

想把今天的内容立刻用起来?逐条对照:

检查项

旧做法

2026 做法

组件声明

React.FC<Props>

(props: Props) => JSX

children

React.FC 隐式包含

显式写 children?: ReactNode

JSX Transform

import React from 'react'

无需 import,配置 react-jsx

异步状态

loading/data/error 分散字段

可辨识联合类型

通用组件

any 或写多份

泛型组件 <T,>

Hook 返回

直接 return 数组

return [...] as const

严格模式

默认配置

开启 strict + 额外检查

运行时校验

Zod/valibot 配合 TypeScript

最后说几句

TypeScript 本质上是给你的代码系了一条安全带。系安全带的时候觉得有点不方便,但一旦出了事,它是救你命的东西。

2026年,React + TypeScript 的最佳实践已经相当成熟。这些模式不是让你的代码"看起来更高级",而是让你真正少写 bug、少加班、重构的时候少崩溃。

新的一年,把旧的写法换掉,让 TypeScript 为你多挡一些线上事故。

🎊 大年初一,前端达人送上新春祝福!

马年大吉,祝各位前端同行:

代码无 bug,部署不回滚,需求不改动,上线不加班!

如果今天这篇文章对你有帮助,欢迎点赞 + 分享给你的技术群,让更多前端同学用上 2026 年真正值得用的 TypeScript 模式。你的每一次分享,都是对《前端达人》最好的支持 🙏 关注《前端达人》,每周持续输出高质量前端技术内容,我们下期见!


— 前端达人 · 马年新春特刊 —

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、先搞清楚:TypeScript 为什么配和 React 搭档?
  • 二、新 JSX Transform:从此不用每个文件写 import React
    • 以前为什么要 import React?
    • 现在不需要了
  • 三、告别 React.FC:显式声明 props 才是正道
    • 为什么要放弃 React.FC?
  • 四、泛型组件:一次编写,无数复用
    • 场景:你需要一个通用列表组件
  • 五、可辨识联合类型:告别 undefined 地狱
    • 一个你每天都会遇到的问题
    • 可辨识联合类型:让非法状态不可能存在
  • 六、自定义 Hook 的类型:返回元组别踩坑
    • 场景:封装 localStorage 读写
  • 七、开启严格模式:让 TypeScript 真正帮你抓 Bug
    • tsconfig.json 这几个选项,开了就回不去
  • 八、一张图看清完整的 TypeScript + React 最佳实践
  • 九、快速落地检查清单
  • 最后说几句
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档