- Notifications
You must be signed in to change notification settings - Fork13.1k
Add types for Object.groupBy() and Map.groupBy()#56805
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
Co-authored-by: Nick McCurdy <nick@nickmccurdy.com>
huw commentedDec 16, 2023
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>
bakkot commentedDec 16, 2023
Done. Hope that's alright@karlhorky@nikeee. |
DanielRosenwasser commentedDec 18, 2023
@typescript-bot pack this |
typescript-bot commentedDec 18, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Heya@DanielRosenwasser, I've started to run the tarball bundle task on this PR at547e844. You can monitor the buildhere. |
typescript-bot commentedDec 18, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Hey@DanielRosenwasser, I've packed this intoan installable tgz. You can install it for testing by referencing it in your and then running There is also a playgroundfor this build and annpm module you can use via |
DanielRosenwasser commentedDec 20, 2023
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. |
bakkot commentedDec 20, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
What do you mean by "more precise"?
Similarly
SGTM, done. |
sandersn left a comment
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<KextendsPropertyKey,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 typePartial?
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.
Becauseyou 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 asRecord<K, T[]>would mean that e.g.Object.groupBy(vals, x => x < 5 ? 'small' : 'large') would be typed asalways havingsmall andlarge keys, such thatresult.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 thatMap<K, T[]> hasget: (k: K) => T[] | undefined so was confused by the difference in types.
sandersn commentedJan 3, 2024
@bakkot I'll merge this after you merge from main (or rebaseline, whichever). |
bakkot commentedJan 3, 2024
@sandersn Done. |
nikelborm commentedJan 12, 2024
Hi, I just noticed this and think it needs some improvements, since it doesn't handle cases like these: functiontester<Kextendsnumber|string>(k:K,index:number):Kextendsnumber ?'numbers' :'strings'{return(typeofk==='number') ?'numbers' :'strings';}// expected numbers ✅consta1=tester(123,123);// ^? const a1: "numbers"// expected strings ✅consta2=tester('123',123);// ^? const a1: "strings"// expected error ✅consta3=tester(true,123);// Argument of type 'boolean' is not assignable to parameter of type 'string | number'.constbasic=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 |
bakkot commentedJan 12, 2024
@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. |
nikelborm commentedJan 12, 2024
nikelborm commentedJan 12, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
It neither does "hard" logic functiontester(k:number|string,index:number):k isstring{returntypeofk==='string';}// expected {'true'?: string[], 'false'?: number[]}constbasic=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) functiontester2(k:number|string,index:number):boolean{returntypeofk==='string';}// expected Partial<Record<"true" | "false", (number | string)[]>>constbasic2=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)=>typeofk==='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 constpeople=[{name:'Julie',age:15},{name:'John',age:23}]const{true:adults,false:children}=Object.groupBy(people,(person)=>person.age>=18); |
bakkot commentedJan 12, 2024
@nikelborm You're welcome to send a followup PR and see if the TS team accepts it. |
nikelborm commentedJan 12, 2024
@sandersn what do you think about this? |
huw commentedJan 13, 2024
Hey there 🙂 You’ve raised 2 separate issues here:
|
slorber commentedJun 28, 2024
Returning Partial See for example: We now have to deal with entries with undefined values, which technically is not really possible in practice. Isn't there a way to express a Record where "only keys are Partial", but values are always defined when the key exists? I tried various things but not sure to have a good solution. Here's my last attempt: typeSubsetRecord<KextendsPropertyKey,T>={[PinK]?:T;};declarefunctiongroupBy<KextendsPropertyKey,T>(items:Iterable<T>,keySelector:(item:T,index:number)=>K,):SubsetRecord<K,T[]>typeUser={name:string,age:number,type:"a"|"b"|"c"}constusers:User[]=[{name:"Seb",age:42,type:"a"}]constgroups=groupBy(users,u=>u.type);// ✅ entries do not have undefinedObject.entries(groups)satisfies[string,User[]][]// ✅groups["a"]!.toSorted()// ✅groupBy(users,u=>"Seb")["Seb"]!.toSorted()// ❌ should be forbidden: value can be undefined//@ts-expect-errorgroups["a"].toSorted()// ❌ should be forbidden because keys do not match//@ts-expect-errorgroupBy(users,u=>"Seb")["blabla"]// ❌ should be forbidden: value can be undefined//@ts-expect-errorgroupBy(users,u=>"Seb")["Seb"].toSorted() |
ktkiiski commentedOct 1, 2024
I agree with this. In cases where the type of the group key This all comes down to TypeScript's limits to separate if the property is there at all vs. if they exist with value However, imo the developer experience would be greatly improved if the signature of interfaceObjectConstructor{/** * Groups members of an iterable according to the return value of the passed callback. *@param items An iterable. *@param keySelector A callback which will be invoked for each item in items. */groupBy<KextendsPropertyKey,T>(items:Iterable<T>,keySelector:(item:T,index:number)=>K,):stringextendsK ?Record<K,T[]> :Partial<Record<K,T[]>>;// 👈 changed return type} The change is on the return value of the method: it now conditionally returns the regular "dictionary" type, e.g. Now, the previously mentioned unnecessary This return type still matches the implementation, but is more useful in cases where the grouping key is just the broad type of Also, with this change, it also leaves up to the developer to choose their What do you think? |
bakkot commentedOct 1, 2024
I think having the types incorrectly suggest that every key is present is worse than having the types incorrectly suggest that keys which are present might have value So I do not think that would be a good change. |
Instead of `Partial<Record<string, T>>` or `Partial<Record<number, T>>`. `Partial` is more correct for narrower types (enums, unions, etc.), since not all keys are guaranteed to be present, but it results in needless checks for `undefined` for `string` and `number`. (Code that's interested in checking for `undefined` there should already be using `noUncheckedIndexedAccess`.)Seemicrosoft#56805 (comment)
Instead of `Partial<Record<string, T>>` or `Partial<Record<number, T>>`. `Partial` is more correct for narrower types (enums, unions, etc.), since not all keys are guaranteed to be present, but it results in needless checks for `undefined` for `string` and `number`. (Code that's interested in checking for `undefined` there should already be using `noUncheckedIndexedAccess`.)Seemicrosoft#56805 (comment)



Uh oh!
There was an error while loading.Please reload this page.
Fixes#47171.
The tests are pretty simple because the types are simple, but I'm happy to expand them.
Try them onthe playground.