You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 19:17:08