TypeScript泛型数组无法赋值给联合类型泛型数组的原因
你的直觉不成立的根源是泛型的协变/逆变规则,Column<T> 本身不是协变泛型,因此无法顺着 Adult extends Adult | Child 的关系推导出 Column<Adult> extends Column<Adult | Child>。
先理清类型兼容的基础规则
TS 里泛型的兼容方向完全由泛型参数的使用位置决定:
- 如果泛型参数只出现在输出位置(比如函数返回值、只读属性),那这个泛型是协变的:比如
type ReadonlyBox<T> = { value: T },只要A extends B,就有ReadonlyBox<A> extends ReadonlyBox<B>,这就是你直觉里的子集关系。 - 如果泛型参数只出现在输入位置(比如函数参数),那这个泛型是逆变的:比如
type Setter<T> = (v: T) => void,兼容方向是反过来的——只有B extends A,才有Setter<A> extends Setter<B>。 - 如果泛型参数同时出现在输入和输出位置,那这个泛型是不变的,只有
T完全相等时,两个泛型类型才兼容。
回到react-table的Column类型
Column<T> 里存在大量把T作为函数参数的属性定义,比如行渲染的Cell、取值的accessor、排序的sortType,这类属性的类型大致是(row: T, ...args: any[]) => any,T出现在逆变的输入位置,因此Column<T>是不变泛型:
Column<Adult>只能赋值给Column<Adult>,不能赋值给Column<Adult | Child>Column<Child>只能赋值给Column<Child>,也不能赋值给Column<Adult | Child>
TS 做这个限制是完全合理的,举个会触发运行时错误的反例:
假设你在
Column<Adult>里定义了一个sortType排序函数,逻辑是读取Adult独有的annualIncome(年收入)字段排序。如果TS允许你把这个列配置传给接收Column<Adult | Child>的表格,当表格渲染Child类型的行时,排序函数拿到Child对象读取annualIncome会得到undefined,直接导致排序逻辑崩溃。
你现在的联合类型readonly Column<Adult>[] | readonly Column<Child>[],语义是「这要么是一整套成人列配置,要么是一整套儿童列配置」;而useTable期望的readonly Column<Adult | Child>[]语义是「每一列都能同时处理成人和儿童两种数据」,两者语义本身就存在差异,不兼容是正常的。
- 最安全的方案:类型窄化后分分支调用useTable
通过类型守卫判断persons的元素类型,把代码分成Adult和Child两个分支,每个分支里columns和data的类型完全匹配,不会有类型错误,也不会有运行时风险:const isAdult = (p: Adult | Child): p is Adult => 'annualIncome' in p // 用Adult独有的字段做类型判断 if (persons.every(isAdult)) { // 此分支内persons是Adult[],columns是Column<Adult>[] const tableProps = useTable<Adult>({ columns, data: persons }, useSortBy) // 渲染表格 } else { // 此分支内persons是Child[],columns是Column<Child>[] const tableProps = useTable<Child>({ columns, data: persons }, useSortBy) // 渲染表格 } - 次安全方案:统一列配置的类型约束
如果你的表格列确实只会用到Adult和Child共有的字段,不会访问任何独有属性,可以直接把PersonTablePropData里的columns类型定义为readonly Column<Adult | Child>[],从源头统一类型,写列配置的时候TS会自动校验,阻止你访问类型独有的字段。 - 快速方案:类型断言(不推荐)
如果你完全确定运行时不会出问题,可以直接用类型断言绕过TS检查:
这个方案会丢失类型安全,后续如果有人在列配置里加了访问独有字段的逻辑,TS不会提示,很容易出线上bug。useTable<Adult|Child>({ columns: columns as readonly Column<Adult|Child>[], data: persons }, useSortBy)
内容的提问来源于stack exchange,提问作者Adrian Bielefeldt

