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

TypeScript中如何用泛型指定属性名?函数类型报错求助

泛型使用问题:tabulate函数类型定义报错的解决方法

问题描述

我尝试为以下函数添加类型定义:

const tabulate = <T, U, K extends string, V extends string>(
  key: K,
  value: V,
  reducefn: (result: U, nextValue: T) => U,
  initialValue: U,
  array: { [K]: string; [V]: T }[]
) => {
  return array.reduce((result, nextValue) => {
    result[nextValue[key]] = reducefn(
      result[nextValue[key]] ?? initialValue,
      nextValue[value]
    );
    return result;
  }, {});
};

tabulate("name", "value", (a, b) => a + b, 0, [
  { name: "John", value: 20 },
  { name: "Kate", value: 10 },
  { name: "Kate", value: 30 },
]); // {John: 20, Kate: 40}

需求是key和value可为任意字符串,但数组中的每个对象必须包含与这两个字符串匹配的属性。然而上述代码无法运行,报错信息如下:

A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1170)
'K' only refers to a type, but is being used as a value here.ts(2693)

请问是否可以这样使用泛型?该如何解决这个问题?


问题分析与解决

可以通过调整泛型和类型定义的写法实现需求,原代码的核心问题在于错误地在类型字面量中使用泛型类型作为计算属性名,同时返回值的类型推导也存在问题。

关键问题拆解

  1. 类型字面量写法错误:{ [K]: string; [V]: T }不符合TypeScript语法规则——类型字面量的计算属性名必须是字面量类型或唯一符号类型,不能直接用泛型类型K/V,这是第一个报错的根源。
  2. 返回值类型缺失:reduce的初始值{}会被推导为{}类型,无法添加新属性,后续赋值操作会触发类型报错。
  3. 第二个报错是第一个问题的连锁反应,修正类型定义后会自动消失。

修正后的代码

const tabulate = <T, U, K extends string, V extends string>(
  key: K,
  value: V,
  reducefn: (result: U, nextValue: T) => U,
  initialValue: U,
  array: Array<Record<K, string> & Record<V, T>>
) => {
  return array.reduce((result, nextValue) => {
    const groupKey = nextValue[key];
    result[groupKey] = reducefn(result[groupKey] ?? initialValue, nextValue[value]);
    return result;
  }, {} as Record<string, U>);
};

// 测试调用
const result = tabulate("name", "value", (a, b) => a + b, 0, [
  { name: "John", value: 20 },
  { name: "Kate", value: 10 },
  { name: "Kate", value: 30 },
]);
console.log(result); // {John: 20, Kate: 40}

核心修正点

  • 用Record<K, string> & Record<V, T>替代错误的类型字面量:Record工具类型可以准确描述"包含指定键、对应值为指定类型"的对象,交叉类型&确保数组元素同时拥有K和V两个属性。
  • 显式指定reduce初始值类型为Record<string, U>:解决空对象无法添加属性的类型限制,明确返回值是字符串键对应U类型值的对象。
  • 提取groupKey变量:简化代码逻辑,避免重复访问nextValue[key]。

可选优化:更精确的返回值类型

如果希望返回值的键类型是数组中所有K属性值的联合类型(比如示例中的"John" | "Kate"),可以添加额外泛型参数实现:

const tabulate = <
  T,
  U,
  K extends string,
  V extends string,
  Item extends Record<K, string> & Record<V, T>
>(
  key: K,
  value: V,
  reducefn: (result: U, nextValue: T) => U,
  initialValue: U,
  array: Item[]
) => {
  return array.reduce((result, nextValue) => {
    const groupKey = nextValue[key];
    result[groupKey] = reducefn(result[groupKey] ?? initialValue, nextValue[value]);
    return result;
  }, {} as Record<Item[K], U>);
};

// 此时result的类型会被推导为Record<"John" | "Kate", number>

内容的提问来源于stack exchange,提问作者Adam B.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 06:12:28