为何TypeScript会将该泛型类型参数推断为never?
问题描述
开发表格组件时,已实现InferColumnUtils工具类型用于列定义的样式配置及格式化函数format的参数推断。声明columns数组时,InferColumnUtils能正确推导每个format函数的参数类型;但在map中调用column.format(order[column.id])时,TypeScript会将format的参数类型推断为never。目前仅能通过包含switch的类型守卫函数formatWithType实现正确推断,但该方案冗余度高,需要更简洁的解决方案。
最小复现代码
// 自动生成的类型 export type Order = { id: string; number: number; paymentInformation?: 'AUTHORIZED' | 'EXPIRED' | 'PENDING'; lineItems: Array<Record<string, number>>; }; interface Column<I, K extends keyof I> { id: K; label: string; format: (value: I[K]) => string; } type InferColumnUtils<I extends Record<string, unknown>> = NonNullable< { [_K in keyof I]: Column<I, _K>; }[keyof I] >; const columns: readonly InferColumnUtils<Order>[] = [ { id: 'paymentInformation', label: 'Payment Status', format: (value) => (!value ? 'NO STATUS' : value), }, { id: 'id', label: 'Order Id', format: (value) => `Order #${value}`, }, { id: 'lineItems', label: 'Items', format: (value) => value.length.toString(), }, ]; const orders: Order[] = [ { id: 'foo', paymentInformation: 'AUTHORIZED', lineItems: [], number: 1, }, { id: 'bar', paymentInformation: 'PENDING', lineItems: [], number: 2, }, { id: 'baz', paymentInformation: 'EXPIRED', lineItems: [], number: 3, }, ]; // 此处 TypeScript 报错:argument of type 'string | number | ("AUTHORIZED" | "EXPIRED" | "PENDING" | undefined) | Record<string, number>[]' is not assignable to parameter of type 'never' const createCells = () => orders.map((order) => columns.map((column) => column.format(order[column.id])) ); createCells()
当前冗余解决方案
// 通过 switch 类型守卫让 TypeScript 识别参数类型 export const formatWithType = ( order: Order, column: InferColumnUtils<Order> ) => { const { id, format } = column; switch (id) { case 'paymentInformation': return format(order[id]); case 'id': return format(order[id]); case 'lineItems': return format(order[id]); case 'number': return format(order[id]); } }; const createCells = () => orders.map((order) => columns.map((column) => formatWithType(order, column)));
简洁解决方案
问题根源在于InferColumnUtils<Order>是联合类型,当遍历该类型的数组时,TypeScript无法自动关联每个元素的id属性与对应的format参数类型。可以通过以下两种方式解决:
方案1:使用泛型辅助函数约束类型关联
定义一个泛型函数,明确绑定列的id类型与对应的数据类型,让TypeScript能追踪两者的关联关系:
// 泛型辅助函数,约束列的K类型与Order的键对应 const formatCell = <K extends keyof Order>(order: Order, column: Column<Order, K>) => { return column.format(order[column.id]); }; const createCells = () => orders.map((order) => columns.map(column => formatCell(order, column as Column<Order, keyof Order>)));
方案2:让columns保留具体元素的类型信息
通过泛型工厂函数创建columns,避免将其统一断言为联合类型数组,保留每个列的具体类型:
// 泛型工厂函数,用于创建列数组并保留每个元素的具体类型 function createColumns<I extends Record<string, unknown>>( ...columns: { [K in keyof I]: Column<I, K> }[keyof I][] ) { return columns as readonly typeof columns; } // 使用工厂函数创建columns,TypeScript会自动推断每个列的具体类型 const columns = createColumns<Order>( { id: 'paymentInformation', label: 'Payment Status', format: (value) => (!value ? 'NO STATUS' : value), }, { id: 'id', label: 'Order Id', format: (value) => `Order #${value}`, }, { id: 'lineItems', label: 'Items', format: (value) => value.length.toString(), }, ); // 此时直接调用column.format即可正确推断参数类型 const createCells = () => orders.map((order) => columns.map((column) => column.format(order[column.id])) );
方案2更优雅,因为它不需要额外的类型断言,完全依赖TypeScript的自动类型推断来保留列的具体类型信息,避免了联合类型遍历导致的类型丢失问题。
内容的提问来源于stack exchange,提问作者Lorenzo Del Rosario
相关产品推荐
相关产品推荐

