英文:
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<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
得分: 1
以下是您提供的内容的中文翻译:
要使这个工作起作用,您希望groupBy()不仅在T(数组的元素类型)中是通用的,还在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实用工具类型而不是{ [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.older和groups.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<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[] }
}
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<K, T[]> 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: "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
Looks good. The compiler infers that K is the union "older" | "shortname" | "other" 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论