Repository navigation
Add Array.prototype.groupBy[toMap] to JS lib definitions. #47171
Description
Activity
- addedDomain: lib.d.tsThe issue relates to the different libraries shipped with TypeScriptThe issue relates to the different libraries shipped with TypeScriptEffort: CasualGood issue if you're already used to contributing to the codebase. Harder than "good first issue".Good issue if you're already used to contributing to the codebase. Harder than "good first issue".
on Dec 16, 2021 Open for someone to take a look. I'm not quite sure off the bat what this should look like FWIW.
I think we probably need to make the same assumptions as we do with
Object.entrieswhere we can't usekeyof#38520I asked, and we generally think this might be ok to have the more accurate typing accurately reflecting the source object. Will need a bit of testing out when there's a PR
DanielRosenwasser commented
on Dec 16, 2021 MemberMore actionsYeah, I think we could think of this as closer to
fromEntries; however,fromEntriesdoesn't try to preserve keys at all (and I don't think it even (cleanly) could).DanielRosenwasser commented
on Dec 16, 2021 MemberMore actionsI feel like there is something close you can get with reversed mapped types - we discussed this in a recent design meeting about correlated unions.
#47109 for some description of this.
Orta Therox (@orta) Daniel Rosenwasser (@DanielRosenwasser) Can you assign this to me?
We don't assign issues to external folks, you're welcome to make the PR as long as there isn't an existing one (which you would see in this issue's timeline above) - so go for it
Orta Therox (@orta) Thanks. Its my first time to contribute to TypeScript. So can you give me some tips and how to get started on this issue?
I feel lost in the codebase :DDaniel Rosenwasser (@DanielRosenwasser) I want to work on this issue. It's my first time to contribute to Typescript. Is it possible for you to give me some information?
Daniel Rosenwasser (@DanielRosenwasser)
Yeah, I think we could think of this as closer to
fromEntries; however,fromEntriesdoesn't try to preserve keys at all (and I don't think it even (cleanly) could).You mean there would be a problem with the following overload, or have I misunderstood?
interface ObjectConstructor { fromEntries<K extends PropertyKey, T = any>(entries: Iterable<readonly [K, T]>): Record<K, T>; }
DanielRosenwasser commented
on Feb 24, 2023 MemberMore actionsWell, I don't know if you can have multiple overloads, but that fails in the case of heterogenous property types:
interface ObjectConstructor { fromEntries<K extends PropertyKey, T = any>(entries: Iterable<readonly [K, T]>): Record<K, T>; } Object.fromEntries([["a", 42], ["b", "hello"]]); // errors
Bazyli Brzóska (@niieani) has a PR at #50203 but it fails once the value is not
as const'd or contextually typed.interface ObjectConstructor { fromEntries<KeyValue extends readonly [PropertyKey, any]>( entries: Iterable<KeyValue>, ): [KeyValue] extends [[PropertyKey, any]] ? { [k: string]: KeyValue[1] } : { [K in KeyValue[0]]: KeyValue extends readonly [K, infer V] ? V : never }; } const a = [["a", 42], ["b", "hello"]]; Object.fromEntries(a) // ~ error
Oh, back on-topic, it looks like there were web-compatibility problems with the name Array.prototype.groupBy (issue: tc39/proposal-array-grouping#37) and with the next name they tried, Array.prototype.group (issue: tc39/proposal-array-grouping#44).
It looks like people are now thinking that the next thing to try is adding groupBy/groupToMap functionality as static factory methods on Object and Map, (PR tc39/proposal-array-grouping#47), with no particular provision for subclasses.
We didn't need provision for subclasses anyway …
which makes sense to me because:
- For the
ArrayLiketoobjectversion, you'll basically always want a null prototype to avoid key collisions. - For the
ArrayLiketoMapversion, this keeps the engine devs' jobs relatively simple (there's no way for user code to observe any of the steps between "7. Let map be ! Construct(%Map%)." and "9. Return map.", so they can optimize the heck out of it as long as the entries end up in the correct order, without needing to worry about fallback to a "slow" version) but you can easily override the method on a subclass constructor if you want.
Anyway, the functions on Array.prototype didn't make any specific provision for subclasses, either.
- For the
Daniel Rosenwasser (@DanielRosenwasser) Just FYI, I've addressed these failure cases, though the overall complexity has increased. The strict and sound typing for Object.fromEntries is published in an NPM package
nesity-types.The proposal has moved
Array.prototype.groupBytoArray.groupByandArray.prototype.groupByToMaptoObject.groupBy.Here are the type declarations I'm using for reference (though I'm not sure about the soundness issue):
global { interface ObjectConstructor { groupBy<T>( items: Iterable<T>, callbackfn: (value: T, index: number) => string, ): Record<string, T[]>; } interface MapConstructor { groupBy<T, U>( items: Iterable<T>, callbackfn: (value: T, index: number) => U, ): Map<U, T[]>; } }
If you're committing them here or to another open source project, please attribute me. For example, using git:
git commit --author "Nick McCurdy <nick@nickmccurdy.com>"Reacted by Karl Horky and Alwan Nuha10 remaining items
groupBy<Item, Key extends PropertyKey>( items: Iterable<Item>, keySelector: (item: Item, index: number) => Key, ): Record<Key, Item[]>;
I stumbled upon that: Returning
Recordcould lead to a wrong return type. For example, this is always evaluates to the same key. TypeScript doesn't always know that some branch is effectively dead or that the original collection doesn't map something to a specific group (the simplest case is when it's empty).Using
Partial<Record<Key, Item[]>>as a return type would be more correct, I think.Reacted by huw and Kevin GibbonsI’d like to +1 Niklas Mollenhauer (@nikeee) there, I raised DefinitelyTyped/DefinitelyTyped#67896 yesterday but we agreed it might be an issue for TypeScript core to determine, since it revolves around the utility of
noUncheckedIndexedAccessand whether the design goals of this type lean toward usefulness or correctness.I’ll argue that a very common (or at least very useful) use of
groupBywill be to simulate a.filter()that returns accepted and rejected items, like so:Object.groupBy([2, 4, 6], value => value % 2 === 0 ? "even" : "odd");
I think it is reasonable that some number of these filter-like use-cases will pass an array that may not satisfy all branches of the comparator. In all of these cases, the outputs will be incorrectly typed. The example above will produce
{ even: [2, 4, 6] }but have the type{ even: number[]; odd: number[] }.typeof result.odd satisfies number[], even whennoUncheckedIndexedAccessis enabled.I can understand if it doesn’t make sense to change the output type to
Partial, but in that case perhaps a compiler flag could be useful to simulatenoUncheckedIndexedAccess-like behaviour on this output?(Additionally, the lift of
const { even = [], odd = [] } = Object.groupBy(…)isn’t that high for most developers, I don’t think it’s an unreasonable ask and pretty cleverly uses language features)Well, if the passend array is empty, the result will be an empty object, so the type would be completely wrong if we're not returning a Partial.
I think this isn't even an edge case and it will happen very often. In fact, I ran into this issue when I used a polyfill.
Reacted by huwReacted by freganteAnd since Daniel is already comparing this to
Object.fromEntriesabove, I think Ryan’s reasoning on the type correctness there would probably also apply:It's not correct to use the input type to determine the output type, because knowing what might be in an array is not the same as knowing what's actually in it. This program is legal per the above definitions but unsound:
const arr: Array<["A", 1] | ["B", 2]> = []; let o = fromEntries(arr); let m: number = o.A;
Typing as
Partial<Record<Key, Item[]>>sounds right to me. You can still do destructuring, so it's still usable.Went ahead and opened #56805. I credited @nickmccurdy in the commit per request above; if anyone else who contributed wants to be credited as well I'm happy to add you.
Reacted by huw, Nicky McCurdy, Semro, Brian Zhou, Veyndan Stuart and Gustavo RPSOpened microsoft/TypeScript-DOM-lib-generator#1943 to show why
Partial<>doesn't make sense here.Partial<>is telling TS thatObject.groupBycan generate this:{key: undefined}. Obviously that doesn't happen. It can generate{}or{key: [obj]}and nothing in betweenfregante According to the docs (and the source),
Partialsets every key to optional, not every value to| undefined.I'm pretty sure your problem is with
Object.values, not withObject.groupBy. You can see this if you test for property presence explicitly, as long as you haveexactOptionalPropertyTypesset.What's the point of
Partial<Record<string, T>>though?the result will be an empty object, so the type would be completely wrong if we're not returning a Partial.
This is not wrong, it's perfectly valid:
const x: Record<string, unknown> = {}
It does not need Partial
If you read a few comments up you'll see a motivating example:
Object.groupBy([], fn).someProperty.lengthshould not typecheck without errors, but it would if you omittedPartial. The properties are, in fact, optional. The types should reflect that.This is really confusing. I don't see the difference between what
groupByreturns and just any randomRecordYes the object does not have "every key" defined, so yes
Record<string, unknown[]>['someProperty']might be undefined.It would be (slightly) different if
groupByreturned the type{first: unknown[], someProperty: unknown[]}, then yessomePropertyshould be made optional by usingPartial<{first: unknown[], someProperty: unknown[]}>But
groupBycurrently returnsRecordand to me thePartial<Record>does not make sense, as no Record ensures the presence of the keys.Recordis mainly useful as an intermediate step when mapping types, which is how it's used here. Having the type of an object beRecord<string, V>is always wrong (unless you're using a Proxy), because it claims that the object provides a value for every string, which it cannot do. The fact that TS allows assigning empty objects toRecord<string, V>is an intentional unsoundness.See some more discussion here.
These types are as correct as they can be barring changes to TS's type system (or making them much more complicated). Changing the types to imply that the returned properties are always present would lead to runtime bugs. Leaving them as they are just means sometimes you have to put
!in some places.Reacted by fregante- locked as resolved and limited conversation to collaborators
on Oct 22, 2025
The proposal for
Array.prototype.groupByandArray.prototype.groupByToMapreached stage 3 recently.Search terms: proposal-array-grouping array groupBy ernest
lib Update Request
With advancement into stage 3, it's time for JS engines, transpilers and toolings to start implementing support for new features and provide feedback for the proposal's advancement into stage 4.
Configuration Check
This is so new that it needs to go into
ESNexttarget.Missing / Incorrect Definition
Missing
Array.prototype.groupByandArray.prototype.groupByToMap.Sample Code
Sample code from https://github.com/es-shims/Array.prototype.groupBy
Documentation Link
Proposal: proposal-array-grouping
Shim implementation that can the code can be tested against: es-shims/Array.prototype.groupBy