You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Object.groupBy should not return Partial<Record<string, T>> or Partial<Record<number, T>> #61706
The type definitions for Object.groupBy<K, T> state that it returns a Partial<Record<K, T>>. This is useful if K is a union, enum, etc. - the result is is likely Partial, and the result is consistent with a Partial<Record<K, T>> literal, and having Partial helps prevent mistakes from assuming that every key is present in the resulting record. However, it's unnecessary if K is a unrestricted number or string: there's no way that a declared key within the result can have an undefined value, and the result is more consistent with a Record<string | number, T> literal than a Partial<Record<string | number, T>> literal, and any mistakes are more consistently dealt with using TypeScript's noUncheckedIndexedAccess option.
typeEmployee={name: string,role: 'ic'|'manager'};constemployees: Set<Employee>=newSet();constbyName=Object.groupBy(employees,x=>x.name);for(const[name,nameGroup]ofbyName){// TypeScript gives an error that nameGroup may be undefined,// but it's always definedconsole.log(nameGroup.length);}
/** * Identifies unrestricted strings and numbers (i.e., not unions, not numeric * enums which can broaden to numbers, etc.). */typeIsAnyStringOrNumber<KextendsPropertyKey>=Kextendsstring|number
? stringextendsK
? K
: `${number}` extends `${K}`
? K
: string|numberextendsK
? K
: never
: never;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,): [K]extends[IsAnyStringOrNumber<K>] ? Record<K,T[]> : Partial<Record<K,T[]>>;}
Changing this would introduce a confusing variance break where you can have code that is legal become illegal when an input type becomes narrower, which is usually not possible.
The function groupBy<K extends PropertyKey, T> should return the type Record<K, T[]>, because when K is unrestricted like string, partiality is unnecessary.
Proposal
Change the behavior of Record<K extends PropertyKey, T> when K is a literal union type ('a' | 'b' | 'c') so that:
Objects of type Record<K, T> are allowed to omit some keys at runtime.
Autocompletion and type inference still show all keys from K.
Accessing a missing key returns T | undefined, similar to Partial<Record<K, T>>.
The example is wrong: Object.groupBy does not return an iterable, but a null-prototype object. If you wrap the object with Object.entries and iterate the resulting array, then you get the error.
The underlying issue is that TypeScript doesn't support optional index signatures (see #46969), and compensates by adding undefined to the type of indexed properties. This in turn allows you to add arbitrary properties whose value is actually undefined to the object. These properties would be present in the array returned by Object.entries.
Thankfully in this case, you can just use Map.groupBy instead, which is arguably more appropriate when you have arbitrary keys.
A particularly awkward consequence is that the optionality leaks into enumeration, where undefined is impossible at runtime:
constgroups=Object.groupBy(items,item=>item.category);// Partial<Record<string, Item[]>>for(constgroupofObject.values(groups)){group.length;// error: 'group' is possibly 'undefined'}
Object.values() only returns values of properties that actually exist, and Object.groupBy() never creates an empty or undefined group. This forces either a redundant runtime check or a non-null assertion on every iteration.
It would be useful to distinguish bounded key unions from wide index-signature keys:
This preserves the important optionality for "a" | "b" while avoiding false undefineds for ordinary dynamic string/number keys. It also aligns naturally with noUncheckedIndexedAccess: dynamic property lookup can remain checked when that option is enabled, without incorrectly affecting Object.values() and Object.entries().
Activity
joshkel commentedon May 15, 2025
A possible implementation of this change:
RyanCavanaugh commentedon May 15, 2025
Changing this would introduce a confusing variance break where you can have code that is legal become illegal when an input type becomes narrower, which is usually not possible.
TiAlRo commentedon Oct 22, 2025
The function
groupBy<K extends PropertyKey, T>should return the typeRecord<K, T[]>, because whenKis unrestricted likestring, partiality is unnecessary.Proposal
Change the behavior of
Record<K extends PropertyKey, T>whenKis a literal union type ('a' | 'b' | 'c') so that:Objects of type
Record<K, T>are allowed to omit some keys at runtime.Autocompletion and type inference still show all keys from
K.Accessing a missing key returns
T | undefined, similar toPartial<Record<K, T>>.Existing
Record<string, T>behavior remains unchanged.Rationale
Matches the behavior of
Record<string, T>for union-literal keys.Reduces friction for APIs like
groupBy, where a union-returning keySelector may not produce all keys in a particular array.Avoids the need for partiality in common scenarios, while keeping type safety.
Optional enhancements
Could allow a compiler flag or strict mode for backward compatibility.
TiAlRo commentedon Oct 22, 2025
Additionally, the type
Partial<Record<string, T>>should be assignable to the typeRecord<string, T>because there is no difference.DominoPivot commentedon Feb 25, 2026
The example is wrong:
Object.groupBydoes not return an iterable, but a null-prototype object. If you wrap the object withObject.entriesand iterate the resulting array, then you get the error.The underlying issue is that TypeScript doesn't support optional index signatures (see #46969), and compensates by adding
undefinedto the type of indexed properties. This in turn allows you to add arbitrary properties whose value is actuallyundefinedto the object. These properties would be present in the array returned byObject.entries.Thankfully in this case, you can just use
Map.groupByinstead, which is arguably more appropriate when you have arbitrary keys.erictheswift commentedon Aug 14, 2026
A particularly awkward consequence is that the optionality leaks into enumeration, where
undefinedis impossible at runtime:Object.values()only returns values of properties that actually exist, andObject.groupBy()never creates an empty orundefinedgroup. This forces either a redundant runtime check or a non-null assertion on every iteration.It would be useful to distinguish bounded key unions from wide index-signature keys:
This preserves the important optionality for
"a" | "b"while avoiding falseundefineds for ordinary dynamicstring/numberkeys. It also aligns naturally withnoUncheckedIndexedAccess: dynamic property lookup can remain checked when that option is enabled, without incorrectly affectingObject.values()andObject.entries().erictheswift commentedon Aug 14, 2026
Map.groupBy is a good workaround. I guess even superior one so we may consider current api as an implicit nudge towards it ;)