📝 引言
📌 场景/痛点
你一定用过 Ant Design 或 Material UI,但你有没有想过它们是怎么做到的?
比如一个 <Button> 组件:
- 它可以渲染成
<a>标签,也可以渲染成<button>标签(这叫 多态组件 / Polymorphic Component)。 - 当它变成
<a>时,必须有href属性;变成<button>时,必须有type属性。TypeScript 怎么知道当前应该校验哪个? - 再比如一个
<Table>,传入User[]数据,列定义里就只能选User的字段,不能乱选。
如果用普通的 any 或 React.FC 写,这简直是噩梦。但用上 TS 5.9 的高级特性,我们可以写出既灵活又类型严密的顶级组件库。
✨ 最终效果
掌握本文后,你将能写出一个通用的 <Box> 组件,它能变成任何 HTML 标签,并且拥有完美的类型提示。
📖 内容概览
本系列收官之作,我们将综合演练:
- 泛型组件:让组件支持动态数据类型。
- 多态组件 :实现
as属性,让组件"变身"。 - 属性智能合并:解决自定义属性与原生属性冲突的问题。
- 系列总结:回顾我们的 20 篇文章进阶之路。
🛠️ 正文
1. 环境准备
使用 React + TypeScript (Vite)。
npm create vite@latest my-lib -- --template react-ts
2. 实战一:泛型数据组件
假设我们要写一个通用的 <Select> 下拉框组件。
tsx
// components/Select.tsx
// 1. 定义泛型 T,代表选项的数据类型
interface SelectProps<T> {
options: T[];
// labelKey 和 valueKey 必须是 T 的 key
labelKey: keyof T;
valueKey: keyof T;
value: T[keyof T];
onChange: (value: T[keyof T]) => void;
}
export function Select<T extends object>({
options,
labelKey,
valueKey,
value,
onChange
}: SelectProps<T>) {
return (
<select
value={String(value)}
onChange={(e) => onChange(e.target.value as T[keyof T])}
>
{options.map((opt, idx) => (
<option key={idx} value={String(opt[valueKey])}>
{/* ✅ 类型安全:opt[labelKey] 必定存在 */}
{String(opt[labelKey])}
</option>
))}
</select>
);
}
// 使用
interface User {
id: number;
name: string;
}
const users: User[] = [{ id: 1, name: "Alice" }];
// ✅ IDE 会提示 labelKey 只能是 'id' | 'name'
<Select
options={users}
labelKey="name"
valueKey="id"
value={1}
onChange={(v) => console.log(v)} // v 的类型自动推导为 number | string
/>
3. 实战二:多态组件 (Polymorphic Component)
这是难点中的难点。我们要实现一个 <Box> 组件,它默认是 div,但可以通过 as 属性变成 button、a、section 等。
3.1 核心类型定义
我们需要利用条件类型 和 React.ElementType 来实现类型分发。
tsx
// components/Box.tsx
import React from 'react';
// 1. 定义自己的属性
type BoxOwnProps<E extends React.ElementType> = {
as?: E; // 允许用户指定标签
children?: React.ReactNode;
};
// 2. 提取原生标签的属性 (如 button 的 type, href)
type NativeProps<E extends React.ElementType> = React.ComponentPropsWithoutRef<E>;
// 3. 智能合并属性:如果自定义属性和原生属性重名(如 'as'),优先使用自定义的
// 这里使用了 Omit 排除掉冲突,再取并集
type BoxProps<E extends React.ElementType> = BoxOwnProps<E> &
Omit<NativeProps<E>, keyof BoxOwnProps<E>>;
// 4. 组件实现
export const Box = <E extends React.ElementType = 'div'>({
as,
children,
...rest
}: BoxProps<E>) => {
// 如果没传 as,默认 'div'
const Component = as || 'div';
// ⚠️ 注意:这里使用 as any 是为了绕过 React 对 spread rest 属性的严格检查
// 在实际的高级库源码中,会用更复杂的类型断言,但这里为了可读性简化
return <Component {...(rest as any)}>{children}</Component>;
};
3.2 使用多态组件
tsx
import { Box } from './Box';
// 场景 1:作为 div 使用
<Box className="p-4">Hello</Box>
// ✅ className 有效
// 场景 2:作为 button 使用
// ✅ 自动获得 onClick 类型提示
<Box as="button" onClick={() => alert('Hi')}>Click Me</Box>
// 场景 3:作为 a 标签使用
// ✅ 自动获得 href 类型提示,且如果不写 href 会报错(如果 html 要求的话)
<Box as="a" href="/home">Go Home</Box>
// 场景 4:作为自定义组件使用
// 甚至可以传 React 组件
<Box as={MyCustomLink}>Custom</Box>
4. 进阶技巧:使用 satisfies 定义 Props
在定义复杂的 Props 时,使用 satisfies 可以保持代码整洁。
tsx
type ButtonVariant = 'primary' | 'secondary';
const buttonStyles = {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-200 text-black'
} satisfies Record<ButtonVariant, string>;
// ✅ 如果写错类型,TS 会直接报错
// const styles = { outline: "bg-gray-100" } satisfies Record<ButtonVariant, string>;
❓ 常见问题
Q1: 为什么我用了 as 属性,TS 还是报错 Property 'href' does not exist on type...?
A: 这通常是因为 Omit 没写对,或者你给 rest 属性的类型推断失败了。确保你的 BoxProps 正确地继承了 React.ComponentProps<E>。在极其复杂的场景下,可以使用 type-fest 库中的 OmitDistribute 或 PolymorphicComponentProps 类型。
Q2: 多态组件会影响性能吗?
A: TypeScript 的泛型类型只在编译时 存在。最终编译出来的 JS 代码中,<Box> 只是一个普通的组件,as 只是一个传递给组件的 props。所以零运行时开销。
Q3: 泛型组件可以在 Vue 3 中使用吗?
A: 完全可以!Vue 3 的 <script setup lang="ts"> 支持 defineGenericComponent (在 3.3+),原理和 React 类似,都是利用泛型参数 T 来约束 Props。
🎯 总结:写在最后
恭喜你!从第一篇的 hello world 到这一篇的多态组件,你已经完成了从 TypeScript 小白到高手的蜕变。
在这 20 篇文章中,我们一起经历了:
- 筑基 :环境搭建、基础类型、字面量与
as const。 - 进阶:类型守卫、联合/交叉类型、映射类型、模板字面量。
- 精通 :
infer、satisfies、NoInfer、工具类型。 - 架构:模块化、声明文件、性能优化、标准装饰器。
- 全栈与前沿:Prisma 共享、Node.js ESM、品牌类型、UI 组件库。
TypeScript 不仅仅是类型的补丁,它是一种思维方式。
它教会我们在写代码之前先思考结构,在运行代码之前先预判错误。在 2026 年及未来,掌握这种思维,是你作为软件工程师最核心的竞争力。
🎉 系列完结
感谢你的陪伴与坚持。技术之路漫漫,愿 TypeScript 成为你手中最锋利的剑。
如果这个系列对你有帮助,请点赞👍、收藏⭐、关注👀,并分享给身边正在学习 TS 的朋友!我们未来再见!