New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add types for Object.groupBy() and Map.groupBy() #56805
Conversation
Co-authored-by: Nick McCurdy <nick@nickmccurdy.com>
I’d probably also credit @karlhorky and @nikeee ❤️ |
Co-authored-by: Karl Horky <karl.horky@gmail.com> Co-authored-by: Niklas Mollenhauer <nikeee@outlook.com>
Done. Hope that's alright @karlhorky @nikeee. |
@typescript-bot pack this |
Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at 547e844. You can monitor the build here. |
Hey @DanielRosenwasser, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
This seems good overall, though users will have to opt in to the more precise inference for keys. I would also think to stick with the same precedent we have elsewhere for type parameter names ( I'll let others weigh in. |
What do you mean by "more precise"?
Similarly
SGTM, done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks right, just one question about Partial.
groupBy<K extends PropertyKey, T>( | ||
items: Iterable<T>, | ||
keySelector: (item: T, index: number) => K, | ||
): Partial<Record<K, T[]>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is the return type Partial
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you might not actually get all of the keys.
As a trivial example, consider the case that the iterable is empty. Then the resulting record will have no keys at all. Typing as Record<K, T[]>
would mean that e.g. Object.groupBy(vals, x => x < 5 ? 'small' : 'large')
would be typed as always having small
and large
keys, such that result.small.length
would check without errors, which is misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, I forgot that Map<K, T[]>
has get: (k: K) => T[] | undefined
so was confused by the difference in types.
@bakkot I'll merge this after you merge from main (or rebaseline, whichever). |
@sandersn Done. |
Hi, I just noticed this and think it needs some improvements, since it doesn't handle cases like these: function tester<K extends number | string>(k: K, index: number): K extends number ? 'numbers' : 'strings' {
return (typeof k === 'number') ? 'numbers' : 'strings';
}
// expected numbers ✅
const a1 = tester(123, 123);
// ^? const a1: "numbers"
// expected strings ✅
const a2 = tester('123', 123);
// ^? const a1: "strings"
// expected error ✅
const a3 = tester(true, 123);
// Argument of type 'boolean' is not assignable to parameter of type 'string | number'.
const basic = Object.groupBy([0, 2, 8, 'asd'], tester);
// ^? const basic: Partial<Record<"numbers" | "strings", (string | number)[]>>
// expected number[] | undefined ❌
basic.numbers
// ^? (property) numbers?: (string | number)[] | undefined
// expected string[] | undefined ❌
basic.strings
// ^? (property) numbers?: (string | number)[] | undefined |
@nikelborm Typescript's types don't generally go for that level of type-level logic. It might be possible in theory but it would significantly complicate the types. The current types aren't wrong, just slightly imprecise. |
It neither does "hard" logic function tester(k: number | string, index: number): k is string {
return typeof k === 'string';
}
// expected {'true'?: string[], 'false'?: number[]}
const basic = Object.groupBy([0, 2, 8, 'asd'], tester);
// Type 'boolean' is not assignable to type 'PropertyKey'. nor easy (it doesn't seems hard to support just booleans) function tester2(k: number | string, index: number): boolean {
return typeof k === 'string';
}
// expected Partial<Record<"true" | "false", (number | string)[]>>
const basic2 = Object.groupBy([0, 2, 8, 'asd'], tester2);
// Type 'boolean' is not assignable to type 'PropertyKey'. It's very common use case to divide an array of something into 2 groups like this: const { true:strings, false: numbers} = Object.groupBy([0, 2, 8, 'asd'], (k) => typeof k === 'string'); It doesn't have to be as simple as checking is this a string or not. It may cover many other business logic use cases const people = [{name: 'Julie', age: 15}, {name: 'John', age: 23}]
const { true: adults, false: children } = Object.groupBy(people, (person) => person.age >= 18); |
@nikelborm You're welcome to send a followup PR and see if the TS team accepts it. |
@sandersn what do you think about this? |
Hey there 🙂 You’ve raised 2 separate issues here:
|
Fixes #47171.
The tests are pretty simple because the types are simple, but I'm happy to expand them.
Try them on the playground.