Skip to content

Commit 6b2c659

Browse files
committed
Object.groupBy can now return Record<string, T> or Record<number, T>
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`.) See microsoft#56805 (comment)
1 parent 0fb5e3a commit 6b2c659

File tree

6 files changed

+142
-42
lines changed

6 files changed

+142
-42
lines changed

src/lib/es2024.object.d.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
/**
2+
* Identifies unrestricted strings and numbers (i.e., not unions, not numeric
3+
* enums which can broaden to numbers, etc.).
4+
*/
5+
type IsAnyStringOrNumber<K extends PropertyKey> = K extends string | number
6+
? string extends K
7+
? K
8+
: `${number}` extends `${K}`
9+
? K
10+
: string | number extends K
11+
? K
12+
: never
13+
: never;
14+
115
interface ObjectConstructor {
216
/**
317
* Groups members of an iterable according to the return value of the passed callback.
@@ -7,5 +21,5 @@ interface ObjectConstructor {
721
groupBy<K extends PropertyKey, T>(
822
items: Iterable<T>,
923
keySelector: (item: T, index: number) => K,
10-
): Partial<Record<K, T[]>>;
24+
): [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>;
1125
}

tests/baselines/reference/objectGroupBy.errors.txt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
objectGroupBy.ts(9,49): error TS2322: Type 'Employee' is not assignable to type 'PropertyKey'.
1+
objectGroupBy.ts(15,49): error TS2322: Type 'Employee' is not assignable to type 'PropertyKey'.
22

33

44
==== objectGroupBy.ts (1 errors) ====
55
const basic = Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large');
66

77
const chars = Object.groupBy('a string', c => c);
88

9-
type Employee = { name: string, role: 'ic' | 'manager' }
9+
enum RoleEnum {
10+
IC,
11+
MANAGER
12+
}
13+
14+
type Employee = { name: string, role: 'ic' | 'manager', roleEnum: RoleEnum };
1015
const employees: Set<Employee> = new Set();
1116
const byRole = Object.groupBy(employees, x => x.role);
17+
const byRoleEnum = Object.groupBy(employees, x => x.roleEnum);
1218

1319
const byNonKey = Object.groupBy(employees, x => x);
1420
~

tests/baselines/reference/objectGroupBy.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,28 @@ const basic = Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large');
55

66
const chars = Object.groupBy('a string', c => c);
77

8-
type Employee = { name: string, role: 'ic' | 'manager' }
8+
enum RoleEnum {
9+
IC,
10+
MANAGER
11+
}
12+
13+
type Employee = { name: string, role: 'ic' | 'manager', roleEnum: RoleEnum };
914
const employees: Set<Employee> = new Set();
1015
const byRole = Object.groupBy(employees, x => x.role);
16+
const byRoleEnum = Object.groupBy(employees, x => x.roleEnum);
1117

1218
const byNonKey = Object.groupBy(employees, x => x);
1319

1420

1521
//// [objectGroupBy.js]
1622
const basic = Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large');
1723
const chars = Object.groupBy('a string', c => c);
24+
var RoleEnum;
25+
(function (RoleEnum) {
26+
RoleEnum[RoleEnum["IC"] = 0] = "IC";
27+
RoleEnum[RoleEnum["MANAGER"] = 1] = "MANAGER";
28+
})(RoleEnum || (RoleEnum = {}));
1829
const employees = new Set();
1930
const byRole = Object.groupBy(employees, x => x.role);
31+
const byRoleEnum = Object.groupBy(employees, x => x.roleEnum);
2032
const byNonKey = Object.groupBy(employees, x => x);

tests/baselines/reference/objectGroupBy.symbols

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,57 @@ const chars = Object.groupBy('a string', c => c);
1717
>c : Symbol(c, Decl(objectGroupBy.ts, 2, 40))
1818
>c : Symbol(c, Decl(objectGroupBy.ts, 2, 40))
1919

20-
type Employee = { name: string, role: 'ic' | 'manager' }
21-
>Employee : Symbol(Employee, Decl(objectGroupBy.ts, 2, 49))
22-
>name : Symbol(name, Decl(objectGroupBy.ts, 4, 17))
23-
>role : Symbol(role, Decl(objectGroupBy.ts, 4, 31))
20+
enum RoleEnum {
21+
>RoleEnum : Symbol(RoleEnum, Decl(objectGroupBy.ts, 2, 49))
22+
23+
IC,
24+
>IC : Symbol(RoleEnum.IC, Decl(objectGroupBy.ts, 4, 15))
25+
26+
MANAGER
27+
>MANAGER : Symbol(RoleEnum.MANAGER, Decl(objectGroupBy.ts, 5, 7))
28+
}
29+
30+
type Employee = { name: string, role: 'ic' | 'manager', roleEnum: RoleEnum };
31+
>Employee : Symbol(Employee, Decl(objectGroupBy.ts, 7, 1))
32+
>name : Symbol(name, Decl(objectGroupBy.ts, 9, 17))
33+
>role : Symbol(role, Decl(objectGroupBy.ts, 9, 31))
34+
>roleEnum : Symbol(roleEnum, Decl(objectGroupBy.ts, 9, 55))
35+
>RoleEnum : Symbol(RoleEnum, Decl(objectGroupBy.ts, 2, 49))
2436

2537
const employees: Set<Employee> = new Set();
26-
>employees : Symbol(employees, Decl(objectGroupBy.ts, 5, 5))
38+
>employees : Symbol(employees, Decl(objectGroupBy.ts, 10, 5))
2739
>Set : Symbol(Set, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.collection.d.ts, --, --))
28-
>Employee : Symbol(Employee, Decl(objectGroupBy.ts, 2, 49))
40+
>Employee : Symbol(Employee, Decl(objectGroupBy.ts, 7, 1))
2941
>Set : Symbol(Set, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.collection.d.ts, --, --))
3042

3143
const byRole = Object.groupBy(employees, x => x.role);
32-
>byRole : Symbol(byRole, Decl(objectGroupBy.ts, 6, 5))
44+
>byRole : Symbol(byRole, Decl(objectGroupBy.ts, 11, 5))
45+
>Object.groupBy : Symbol(ObjectConstructor.groupBy, Decl(lib.es2024.object.d.ts, --, --))
46+
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
47+
>groupBy : Symbol(ObjectConstructor.groupBy, Decl(lib.es2024.object.d.ts, --, --))
48+
>employees : Symbol(employees, Decl(objectGroupBy.ts, 10, 5))
49+
>x : Symbol(x, Decl(objectGroupBy.ts, 11, 40))
50+
>x.role : Symbol(role, Decl(objectGroupBy.ts, 9, 31))
51+
>x : Symbol(x, Decl(objectGroupBy.ts, 11, 40))
52+
>role : Symbol(role, Decl(objectGroupBy.ts, 9, 31))
53+
54+
const byRoleEnum = Object.groupBy(employees, x => x.roleEnum);
55+
>byRoleEnum : Symbol(byRoleEnum, Decl(objectGroupBy.ts, 12, 5))
3356
>Object.groupBy : Symbol(ObjectConstructor.groupBy, Decl(lib.es2024.object.d.ts, --, --))
3457
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
3558
>groupBy : Symbol(ObjectConstructor.groupBy, Decl(lib.es2024.object.d.ts, --, --))
36-
>employees : Symbol(employees, Decl(objectGroupBy.ts, 5, 5))
37-
>x : Symbol(x, Decl(objectGroupBy.ts, 6, 40))
38-
>x.role : Symbol(role, Decl(objectGroupBy.ts, 4, 31))
39-
>x : Symbol(x, Decl(objectGroupBy.ts, 6, 40))
40-
>role : Symbol(role, Decl(objectGroupBy.ts, 4, 31))
59+
>employees : Symbol(employees, Decl(objectGroupBy.ts, 10, 5))
60+
>x : Symbol(x, Decl(objectGroupBy.ts, 12, 44))
61+
>x.roleEnum : Symbol(roleEnum, Decl(objectGroupBy.ts, 9, 55))
62+
>x : Symbol(x, Decl(objectGroupBy.ts, 12, 44))
63+
>roleEnum : Symbol(roleEnum, Decl(objectGroupBy.ts, 9, 55))
4164

4265
const byNonKey = Object.groupBy(employees, x => x);
43-
>byNonKey : Symbol(byNonKey, Decl(objectGroupBy.ts, 8, 5))
66+
>byNonKey : Symbol(byNonKey, Decl(objectGroupBy.ts, 14, 5))
4467
>Object.groupBy : Symbol(ObjectConstructor.groupBy, Decl(lib.es2024.object.d.ts, --, --))
4568
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
4669
>groupBy : Symbol(ObjectConstructor.groupBy, Decl(lib.es2024.object.d.ts, --, --))
47-
>employees : Symbol(employees, Decl(objectGroupBy.ts, 5, 5))
48-
>x : Symbol(x, Decl(objectGroupBy.ts, 8, 42))
49-
>x : Symbol(x, Decl(objectGroupBy.ts, 8, 42))
70+
>employees : Symbol(employees, Decl(objectGroupBy.ts, 10, 5))
71+
>x : Symbol(x, Decl(objectGroupBy.ts, 14, 42))
72+
>x : Symbol(x, Decl(objectGroupBy.ts, 14, 42))
5073

tests/baselines/reference/objectGroupBy.types

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ const basic = Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large');
1010
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1111
>Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large') : Partial<Record<"small" | "large", number[]>>
1212
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13-
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
14-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
13+
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
14+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
1515
>Object : ObjectConstructor
1616
> : ^^^^^^^^^^^^^^^^^
17-
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
18-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
17+
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
18+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
1919
>[0, 2, 8] : number[]
2020
> : ^^^^^^^^
2121
>0 : 0
@@ -42,16 +42,16 @@ const basic = Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large');
4242
> : ^^^^^^^
4343

4444
const chars = Object.groupBy('a string', c => c);
45-
>chars : Partial<Record<string, string[]>>
46-
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47-
>Object.groupBy('a string', c => c) : Partial<Record<string, string[]>>
48-
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49-
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
50-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
45+
>chars : Record<string, string[]>
46+
> : ^^^^^^^^^^^^^^^^^^^^^^^^
47+
>Object.groupBy('a string', c => c) : Record<string, string[]>
48+
> : ^^^^^^^^^^^^^^^^^^^^^^^^
49+
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
50+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
5151
>Object : ObjectConstructor
5252
> : ^^^^^^^^^^^^^^^^^
53-
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
54-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
53+
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
54+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
5555
>'a string' : "a string"
5656
> : ^^^^^^^^^^
5757
>c => c : (c: string) => string
@@ -61,13 +61,28 @@ const chars = Object.groupBy('a string', c => c);
6161
>c : string
6262
> : ^^^^^^
6363

64-
type Employee = { name: string, role: 'ic' | 'manager' }
64+
enum RoleEnum {
65+
>RoleEnum : RoleEnum
66+
> : ^^^^^^^^
67+
68+
IC,
69+
>IC : RoleEnum.IC
70+
> : ^^^^^^^^^^^
71+
72+
MANAGER
73+
>MANAGER : RoleEnum.MANAGER
74+
> : ^^^^^^^^^^^^^^^^
75+
}
76+
77+
type Employee = { name: string, role: 'ic' | 'manager', roleEnum: RoleEnum };
6578
>Employee : Employee
6679
> : ^^^^^^^^
6780
>name : string
6881
> : ^^^^^^
6982
>role : "ic" | "manager"
7083
> : ^^^^^^^^^^^^^^^^
84+
>roleEnum : RoleEnum
85+
> : ^^^^^^^^
7186

7287
const employees: Set<Employee> = new Set();
7388
>employees : Set<Employee>
@@ -82,12 +97,12 @@ const byRole = Object.groupBy(employees, x => x.role);
8297
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8398
>Object.groupBy(employees, x => x.role) : Partial<Record<"ic" | "manager", Employee[]>>
8499
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85-
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
86-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
100+
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
101+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
87102
>Object : ObjectConstructor
88103
> : ^^^^^^^^^^^^^^^^^
89-
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
90-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
104+
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
105+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
91106
>employees : Set<Employee>
92107
> : ^^^^^^^^^^^^^
93108
>x => x.role : (x: Employee) => "ic" | "manager"
@@ -101,17 +116,41 @@ const byRole = Object.groupBy(employees, x => x.role);
101116
>role : "ic" | "manager"
102117
> : ^^^^^^^^^^^^^^^^
103118

119+
const byRoleEnum = Object.groupBy(employees, x => x.roleEnum);
120+
>byRoleEnum : Partial<Record<RoleEnum, Employee[]>>
121+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
122+
>Object.groupBy(employees, x => x.roleEnum) : Partial<Record<RoleEnum, Employee[]>>
123+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
124+
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
125+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
126+
>Object : ObjectConstructor
127+
> : ^^^^^^^^^^^^^^^^^
128+
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
129+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
130+
>employees : Set<Employee>
131+
> : ^^^^^^^^^^^^^
132+
>x => x.roleEnum : (x: Employee) => RoleEnum
133+
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^
134+
>x : Employee
135+
> : ^^^^^^^^
136+
>x.roleEnum : RoleEnum
137+
> : ^^^^^^^^
138+
>x : Employee
139+
> : ^^^^^^^^
140+
>roleEnum : RoleEnum
141+
> : ^^^^^^^^
142+
104143
const byNonKey = Object.groupBy(employees, x => x);
105144
>byNonKey : Partial<Record<PropertyKey, Employee[]>>
106145
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
107146
>Object.groupBy(employees, x => x) : Partial<Record<PropertyKey, Employee[]>>
108147
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
109-
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
110-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
148+
>Object.groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
149+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
111150
>Object : ObjectConstructor
112151
> : ^^^^^^^^^^^^^^^^^
113-
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => Partial<Record<K, T[]>>
114-
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
152+
>groupBy : <K extends PropertyKey, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K) => [K] extends [IsAnyStringOrNumber<K>] ? Record<K, T[]> : Partial<Record<K, T[]>>
153+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^^^^
115154
>employees : Set<Employee>
116155
> : ^^^^^^^^^^^^^
117156
>x => x : (x: Employee) => Employee

tests/cases/compiler/objectGroupBy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ const basic = Object.groupBy([0, 2, 8], x => x < 5 ? 'small' : 'large');
44

55
const chars = Object.groupBy('a string', c => c);
66

7-
type Employee = { name: string, role: 'ic' | 'manager' }
7+
enum RoleEnum {
8+
IC,
9+
MANAGER
10+
}
11+
12+
type Employee = { name: string, role: 'ic' | 'manager', roleEnum: RoleEnum };
813
const employees: Set<Employee> = new Set();
914
const byRole = Object.groupBy(employees, x => x.role);
15+
const byRoleEnum = Object.groupBy(employees, x => x.roleEnum);
1016

1117
const byNonKey = Object.groupBy(employees, x => x);

0 commit comments

Comments
 (0)