
🎊 大年初一特刊 · 前端达人新春献礼
前几天帮朋友 Code Review,看到一段 2024 年写的 React 代码:
const UserCard: React.FC<{ name: string }> = ({ name, children }) => {
return <div>{name}{children}</div>
}
第一反应是:这代码没问题啊,能跑。但仔细一想——children 是哪来的?React.FC 自带了隐式的 children 类型,你以为写了个只接收 name 的组件,实际上任何人都可以往里塞任何内容,TypeScript 不会报一个字的错。
这就好比你家门上挂了把锁,但有人发现侧面开了一扇暗门,进出自由,锁形同虚设。
这就是 TypeScript 用了等于没用的典型案例。
2026年,这些问题早有更好的解法。今天这篇文章,我们就系统梳理一遍 React + TypeScript 在当下真正值得用的模式——不是拿来炫技的,而是每天都会用到、用了就回不去的那种。
打个比方:写 JavaScript 的 React 项目,就像在没有地图的城市里开车。你大概知道方向,但每次拐弯都要猜一猜——这个 props 到底传了什么?这个函数的返回值是啥类型?出了 bug 才发现原来走错了路。
TypeScript 给你装上了导航。它在你写代码的时候就告诉你:这里不对,那里缺了什么,向右转有一条更快的路。
2025年,JS 生态里的 TypeScript 采用率已经到了 78%(State of JS 数据),大厂项目几乎清一色 TypeScript。字节跳动内部许多 React 项目,Ant Design、arco-design 这些组件库,全部基于 TypeScript 构建。你会 TypeScript,不只是加分项,是基础门槛。
好,背景说完,直接进正题。
import ReactReact 17 之前,写 JSX 其实是在写这个:
// 你写的
const el = <div>hello</div>
// 编译器翻译成
const el = React.createElement('div', null, 'hello')
所以必须引入 React,不然 React.createElement 找不到,报错。
React 17+ 引入了新的 JSX Transform,编译器会自动从 react/jsx-runtime 引入需要的函数,不再依赖全局的 React 变量。
// ✅ 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 配置正确:
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
💡 很多同学升级了 React 版本,但忘记更新 tsconfig,导致项目里还是一堆多余的
import React,虽然不影响运行,但是冗余代码,代码评审会被挑。
React.FC:显式声明 props 才是正道React.FC?React.FC 这个类型有两个主要问题:
问题一:隐式包含 children(React 18 之前)
// ❌ 旧写法:React.FC 在 React 17 及更早版本中隐式包含 children
const Card: React.FC<{ title: string }> = ({ title, children }) => {
// children 哪来的?React.FC 帮你偷偷加上了
return <div>{title}{children}</div>
}
就好比你订了一份外卖,只点了宫保鸡丁,结果送来的时候多了一碗不知道啥口味的汤,你没点,但它来了,你也不知道该怎么处理它。
问题二:表达能力不够强
显式声明 props interface,你能更精确地控制每个 prop 的类型和是否必填,代码意图一目了然。
// ✅ 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 等于没用。
正确答案:泛型组件。
用户需求
|
v
┌─────────────────────────────────────┐
│ <List<T>> 泛型组件 │
│ │
│ items: T[] ──→ 遍历每一项 │
│ renderItem: (item: T) => JSX │
│ keyExtractor: (item: T) => string │
└─────────────────────────────────────┘
| | |
v v v
商品列表 订单列表 用户列表
Product[] Order[] User[]
// 定义泛型列表组件
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 完全自动推断类型:
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 在每次使用时自动帮你核查类型,无需重复声明。
请求数据时,组件有几种状态:加载中、成功、失败。
很多人这样写:
// ❌ 容易出问题的写法
interface State {
loading: boolean;
data?: User[];
error?: string;
}
问题来了:loading: false + data: undefined + error: undefined ——这是初始状态还是请求失败了?loading: false + data: [...] + error: "网络错误" ——同时有数据又有错误,这是什么情况?
这些"非法状态"TypeScript 完全无法拦截。
┌── { status: 'idle' }
│
状态只能是 ──┼── { status: 'loading' }
│
├── { status: 'success'; data: User[] }
│
└── { status: 'error'; error: string }
每种状态下 TypeScript 精确知道有哪些字段
非法状态在类型层面根本无法构造
// ✅ 可辨识联合类型
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。
// 返回元组,用 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?
// 不加 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 ✅
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true
}
}
用一个比喻来解释这些配置的作用:
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 替你提前发现了。
┌─────────────────────────────────────────────────────────────┐
│ 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 模式。你的每一次分享,都是对《前端达人》最好的支持 🙏 关注《前端达人》,每周持续输出高质量前端技术内容,我们下期见!
— 前端达人 · 马年新春特刊 —