Is there a way to change this typescript function so that the resultant object is typed with only the keys in the map?

huangapple go评论75阅读模式
英文:

Is there a way to change this typescript function so that the resultant object is typed with only the keys in the map?

问题

这个函数将返回一个类型为{[key: string]: T[]}的对象。

但是我想要提高这个函数的类型安全性,以便在IDE中能够智能提示生成的对象中的有效键。

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

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()) as {[key: string]: T[]};
}
英文:

The function will return an object of type {[key: string] : T[]}
But I am looking to improve the type safety of this function so that in the IDE I get IntelliSense on what keys are valid in the resulting object.

I've been trying to get the keys as a const value and use that to type the object, but I can't figure out the correct syntax to get it working, nor do I know if that is even a correct approach.

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

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

答案1

得分: 1

以下是您提供的内容的中文翻译:

要使这个工作起作用,您希望groupBy()不仅在T(数组的元素类型)中是通用的,还在KgetKey()的返回类型)中是通用的。像这样:

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()时,编译器将尝试推断KgetKey()可以返回的特定值对应的字符串字面类型的联合类型。

然后,groupBy()的返回类型是{ [P in K]: T[] }(使用Record实用工具类型而不是{ [k: string]: T[] }等效)。这意味着它具有类型为K的键和类型为T[]的值。请注意,我需要断言返回的值由Object.entries()生成,因为TypeScript编译器无法验证这一点(请参阅https://stackoverflow.com/q/62053739/2887218或其他问题以获取更多信息)。


让我们通过一个示例来测试它:

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.oldergroups.shortname,但不允许您索引groups.randomKey,这应该是您期望的行为。

英文:

For this to work you want groupBy() to be generic not only in T, the element type of array, but also in K, the return type of getKey(). Like this:

function groupBy&lt;T, K extends string&gt;(array: T[], getKey: (item: T) =&gt; K) {
  const map = new Map&lt;string, T[]&gt;();
  array.forEach((item) =&gt; {
    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[] }
}

Here K is constrained to string, so the compiler will complain if getKey() returns a non-string. But now when you call groupBy(), the compiler will try to infer K to be a union of string literal types corresponding to the particular values that getKey() can return.

Then the return type of groupBy() is { [P in K]: T[] } (equivalent to Record&lt;K, T[]&gt; using the Record utility type instead of { [k: string]: T[] }. That means it has keys of type K and values of type T[]. Note that I needed to assert that the value returned by Object.entries() is of that type, since the TypeScript compiler can't verify that (see https://stackoverflow.com/q/62053739/2887218 or other questions for more information).


Let's test it out with an example:

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

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

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

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

console.log(&quot;Shortname: &quot; + groups.shortname.map(foo =&gt; foo.name).join(&quot;, &quot;))
// Shortname: Bob

groups.randomKey; // error

Looks good. The compiler infers that K is the union &quot;older&quot; | &quot;shortname&quot; | &quot;other&quot; and therefore that the returned groups value is of type {older: Foo[]; shortname: Foo[]; other: Foo[]}. And so it lets you index into groups.older and groups.shortname but it wouldn't let you index into groups.randomKey, which is I think what your desired behavior is.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月6日 08:30:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76410738.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定