TypeScript数组字段类型约束不严格问题排查与修复
我原本希望通过泛型实现如下约束:DataPresentation 组件的 searchKey 属性仅可传入 columns 配置中 field 字段的合法取值,相关类型定义代码如下:
type Props<RowType extends AnyObject, ColType extends string> = { columns: ReadonlyArray<{ readonly field: ColType }>; rows: RowType[]; searchKey?: ColType; }; const DataPresentation = <RowType extends AnyObject, ColType extends string>({ columns, rows, searchKey, }: Props<RowType, ColType>) => { /* 组件实现逻辑 */ }
组件的使用代码如下:
export const playerPropsColumns = [ { field: 'sportId' }, // todo: get sport name. from backend? { field: 'playerName' }, { field: 'line' }, { field: 'status' }, ] as const; // 组件内使用 <DataPresentation columns={playerPropsColumns} rows={data} searchKey={'XXXXX'} />
上述代码中传入的非法searchKey值居然通过了类型校验,且实际现象并非简单的类型约束失效:鼠标悬停查看searchKey的类型时,其推断结果为:
searchKey?: "line" | "sportId" | "playerName" | "status" | "XXXXX" | undefined
同时playerPropsColumns的推断类型为:
columns: readonly {readonly field: "line" | "sportId" | "playerName" | "status" | "XXXXX"}[]
可见类型系统确实从playerPropsColumns数组中提取了合法的field取值,但同时也将传入searchKey的任意值加入了允许的联合类型范围,完全丧失了类型校验的作用。
补充说明:代码中所有readonly修饰符是为了在定义columns时使用as const断言,从而支持通过type FieldType = typeof playerPropsColumns[number]['field'];提取字段类型。但完成泛型封装后我发现无需单独定义该类型,因此是否可以移除readonly相关修饰?该修饰是否是导致问题的原因?
这是TypeScript泛型推断的默认行为导致的:当同一个泛型参数同时被多个位置的类型约束引用时,TS会尝试推断出同时满足所有位置约束的联合类型,而不是优先从某一个信任的位置推断类型再校验其他位置。
在这个场景里,ColType同时被columns[number].field和searchKey两个位置引用:
- 从
columns位置TS能推断出ColType应该是"line" | "sportId" | "playerName" | "status" - 从
searchKey位置TS能推断出ColType应该包含"XXXXX"
最终TS会把两个结果合并成联合类型,相当于把非法的XXXXX也纳入了合法值范围,自然不会抛类型错误。
注意:代码里的readonly修饰符和as const不是问题诱因,这部分逻辑是为了锁定字面量类型,反而是实现精准类型推断必须的配置,不能移除。
核心思路是阻止TS从searchKey位置反向推断ColType泛型,让ColType的类型完全由columns的传入值决定,再用推断出的固定类型去校验searchKey的传入值。
可以用TS内置的NoInfer工具类型(TS 5.4+内置,低版本可以自己实现)包裹searchKey的类型定义,修改后的Props类型如下:
// TS 5.4及以上版本直接用内置NoInfer type Props<RowType extends AnyObject, ColType extends string> = { columns: ReadonlyArray<{ readonly field: ColType }>; rows: RowType[]; searchKey?: NoInfer<ColType>; // 这里加NoInfer包裹 }; // 低版本TS如果没有内置NoInfer,自行实现即可 type NoInfer<T> = T extends infer U ? U : never;
修改后再传入非法的searchKey值,TS会直接抛出类型错误,ColType的类型只会从columns配置中提取,不会被searchKey的传入值污染。
如果是更老的TS版本,也可以通过增加泛型参数的冗余约束实现同样效果,不过NoInfer是最直观、维护成本最低的方案。
内容的提问来源于stack exchange,提问作者Jonathan Tuzman

