网站搜索

javascript - 有没有办法修改这个 TypeScript 函数,使得生成的对象只包含 map 中的键的类型?


该函数将返回类型为 {[key: string] : T[]} 的对象。但是我希望提高该函数的类型安全性,以便在 IDE 中可以获得关于结果对象中哪些键是有效的 IntelliSense 提示。

我一直在尝试将键作为常量值获取,并使用它来定义对象的类型,但我无法弄清楚让它工作的正确语法,也不知道这是否是正确的方法。

export function groupBy<T>(array: T[], getKey: (item: T) => string) {
    const map = new Map<string, T[]>();
    array.forEach((item) => {
        const key = getKey(item);
        const group = map.get(key);
        group ? group.push(item) : map.set(key, [item]);
    });

    return Object.fromEntries(map.entries());
}

答案#1

为了让这起作用,你希望 groupBy() 不仅在 T(array 的元素类型)上是泛型的,而且在 K(getKey() 的返回类型)上也是泛型的。像这样:

function groupBy<T, K extends string>(array: T[], getKey: (item: T) => K) {
  const map = new Map<string, T[]>();
  array.forEach((item) => {
    const key = getKey(item);
    const group = map.get(key);
    group ? group.push(item) : map.set(key, [item]);
  });

  return Object.fromEntries(map.entries()) as { [P in K]: T[] }
}

这里 K 被限制为 string,所以如果 getKey() 返回非 string 类型,编译器会报错。但现在当你调用 groupBy() 时,编译器会尝试将 K 推断为 getKey() 可能返回的特定值对应的字符串字面量类型的联合。

那么 groupBy() 的返回类型是 { [P in K]: T[] }(使用 Record 工具类型等同于 Record<K, T[]>,而不是 { [k: string]: T[] })。这意味着它的键的类型是 K,值的类型是 T[]。注意,我需要断言 Object.entries() 返回的值是该类型,因为 TypeScript 编译器无法验证这一点(更多信息请参见使用 Object.entries 时保留类型或其他相关问题)。


让我们用一个例子来测试一下:

interface Foo {
  name: string;
  age: number;
}

const foos: Foo[] = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 35 },
  { name: "Carol", age: 40 },
  { name: "Dave", age: 50 }
];

const groups = groupBy(
  foos,
  (foo) => foo.age >= 40 ? "older" : foo.name.length <= 4 ? "shortname" : "other"
);
/* const groups: {
    older: Foo[];
    shortname: Foo[];
    other: Foo[];
} */

console.log("Older: " + groups.older.map(foo => foo.name).join(", "));
// Older: Carol, Dave

console.log("Shortname: " + groups.shortname.map(foo => foo.name).join(", "))
// Shortname: Bob

groups.randomKey; // error

看起来不错。编译器推断 K 是联合类型 "older" | "shortname" | "other",因此返回的 groups 值的类型是 {older: Foo[]; shortname: Foo[]; other: Foo[]}。因此,它允许你索引 groups.older 和 groups.shortname,但不会让你索引 groups.randomKey,我认为这就是你期望的行为。

代码的 Playground 链接