- Published on
- -6 min read
Type Safe GroupBy In TypeScript
Lodash's groupBy
I would bet if you have a sizeable Javascript/Typescript codebase you most likely are using lodash somewhere in there. While Javascript has gotten more "batteries included" over the last few years, lodash still has many nice functions for manipulating arrays/objects. One such function is groupBy. It groups a list by some predicate, in the simplest case it can just be a key in the objects of the array.
import const _: _.LoDashStatic
_ from 'lodash';
interface Foo {
Foo.num: number
num: number;
Foo.someLiteral: "a" | "b" | "c"
someLiteral: 'a' | 'b' | 'c';
Foo.object: Record<string, any>
object: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<string, any>;
}
const const vals: Foo[]
vals: Foo[] = [
{ Foo.num: number
num: 1, Foo.someLiteral: "a" | "b" | "c"
someLiteral: 'a', Foo.object: Record<string, any>
object: { key: string
key: 'value' } },
{ Foo.num: number
num: 2, Foo.someLiteral: "a" | "b" | "c"
someLiteral: 'a', Foo.object: Record<string, any>
object: { key: string
key: 'diffValue' } },
{ Foo.num: number
num: 1, Foo.someLiteral: "a" | "b" | "c"
someLiteral: 'b', Foo.object: Record<string, any>
object: {} },
];
var console: Console
console.Console.dir(item?: any, options?: any): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)dir(const _: _.LoDashStatic
_.LoDashStatic.groupBy<Foo>(collection: _.List<Foo> | null | undefined, iteratee?: _.ValueIteratee<Foo> | undefined): _.Dictionary<Foo[]> (+1 overload)
Creates an object composed of keys generated from the results of running each element of collection through
iteratee. The corresponding value of each key is an array of the elements responsible for generating the
key. The iteratee is invoked with one argument: (value).groupBy(const vals: Foo[]
vals, 'num'));
/*
{
'1': [ { num: 1, someLiteral: 'a' }, { num: 1, someLiteral: 'b' } ],
'2': [ { num: 2, someLiteral: 'a' } ]
}
*/
var console: Console
console.Console.dir(item?: any, options?: any): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)dir(const _: _.LoDashStatic
_.LoDashStatic.groupBy<Foo>(collection: _.List<Foo> | null | undefined, iteratee?: _.ValueIteratee<Foo> | undefined): _.Dictionary<Foo[]> (+1 overload)
Creates an object composed of keys generated from the results of running each element of collection through
iteratee. The corresponding value of each key is an array of the elements responsible for generating the
key. The iteratee is invoked with one argument: (value).groupBy(const vals: Foo[]
vals, 'someLiteral'));
/*
{
a:[
{ num: 1, someLiteral: 'a', object: [Object] },
{ num: 2, someLiteral: 'a', object: [Object] }
],
b: [ { num: 1, someLiteral: 'b', object: {} } ]
}
*/
This all seems to make sense, you can set what key you want to group on, and you get back an object whose keys are the values for found in the input array of objects.
Now if you're in a TypeScript code base I hope you are using the definitely typed lodash types to add some types to the lodash functions. In this case the _.groupBy
type looks roughly like (simplified from the actual code)
declare function function groupBy<T>(collection: Array<T>, key: string): Dictionary<T[]>
groupBy<function (type parameter) T in groupBy<T>(collection: Array<T>, key: string): Dictionary<T[]>
T>(collection: T[]
collection: interface Array<T>
Array<function (type parameter) T in groupBy<T>(collection: Array<T>, key: string): Dictionary<T[]>
T>, key: string
key: string): interface Dictionary<T>
Dictionary<function (type parameter) T in groupBy<T>(collection: Array<T>, key: string): Dictionary<T[]>
T[]>;
interface interface Dictionary<T>
Dictionary<function (type parameter) T in Dictionary<T>
T> {
[index: string
index: string]: function (type parameter) T in Dictionary<T>
T;
}
So a few things stick out here. First, the key
type is just string, so there's nothing stopping me from doing _.groupBy(vals, "someKeyThatDoesNotExist")
. Second, we have no restrictions at the type level of me grouping on a key whose value is not a valid object key (the value must be a subset of string | number | symbol
). For example in Foo
the object
key's value was a record. Here's what happens when you try to group on that key.
var console: Console
console.Console.dir(item?: any, options?: any): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)dir(const _: _.LoDashStatic
_.LoDashStatic.groupBy<Foo>(collection: _.List<Foo> | null | undefined, iteratee?: _.ValueIteratee<Foo> | undefined): _.Dictionary<Foo[]> (+1 overload)
Creates an object composed of keys generated from the results of running each element of collection through
iteratee. The corresponding value of each key is an array of the elements responsible for generating the
key. The iteratee is invoked with one argument: (value).groupBy(const vals: Foo[]
vals, 'object'));
/*
{
'[object Object]': [
{ num: 1, someLiteral: 'a', object: [Object] },
{ num: 2, someLiteral: 'a', object: [Object] },
{ num: 1, someLiteral: 'b', object: {} }
]
}
*/
In this case the objects where coerced to string values so all elements of vals
where grouped under the same weird [object Object]
key. While this does not throw an error there is almost 0 chance you want this to happen in your code.
Finally, the return type of this function is Dictionary
, while its "right" it could be "more right" by encoding that the returning object's keys would be the values of the grouping key in the input object.
Making our own groupBy
insert Bender joke here
To start making our own type safe groupBy
, we first need some code that actually does the grouping logic. Let's start with that and some basic types.
// Note: PropertyKey is a builtIn type alias of
// type PropertyKey = string | number | symbol
// This lets us use "Record<PropertyKey, any>" to represent any object
// but is slightly nicer to use than the "object" type
function function simpleGroupBy<T extends Record<PropertyKey, any>>(arr: T[], key: keyof T): any
simpleGroupBy<function (type parameter) T in simpleGroupBy<T extends Record<PropertyKey, any>>(arr: T[], key: keyof T): any
T extends type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<type PropertyKey = string | number | symbol
PropertyKey, any>>(arr: T[]
arr: function (type parameter) T in simpleGroupBy<T extends Record<PropertyKey, any>>(arr: T[], key: keyof T): any
T[], key: keyof T
key: keyof function (type parameter) T in simpleGroupBy<T extends Record<PropertyKey, any>>(arr: T[], key: keyof T): any
T): any {
return arr: T[]
arr.Array<T>.reduce<any>(callbackfn: (previousValue: any, currentValue: T, currentIndex: number, array: T[]) => any, initialValue: any): any (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((accumulator: any
accumulator, val: T extends Record<PropertyKey, any>
val) => {
const const groupedKey: T[keyof T]
groupedKey = val: T extends Record<PropertyKey, any>
val[key: keyof T
key];
if (!accumulator: any
accumulator[const groupedKey: T[keyof T]
groupedKey]) {
accumulator: any
accumulator[const groupedKey: T[keyof T]
groupedKey] = [];
}
accumulator: any
accumulator[const groupedKey: T[keyof T]
groupedKey].push(val: T extends Record<PropertyKey, any>
val);
return accumulator: any
accumulator;
}, {} as any);
}
var console: Console
console.Console.dir(item?: any, options?: any): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)dir(function simpleGroupBy<Foo>(arr: Foo[], key: keyof Foo): any
simpleGroupBy(const vals: Foo[]
vals, 'num'));
/*
{
'1': [
{ num: 1, someLiteral: 'a', object: [Object] },
{ num: 1, someLiteral: 'b', object: {} }
],
'2': [ { num: 2, someLiteral: 'a', object: [Object] } ]
}
*/
Cool the logic here seems to work, but obviously the types could use some love.
Let's start by adding a few more generics, so we can type the output correctly. Your first change might be to make the return type Record<string, T[]>
since the keys will be coerced to strings by JavaScript and the values will be the same values in the array. This will unfortunately make typescript sad.
function function sadAttempt<T extends object>(arr: T[], key: keyof T): Record<string, T[]>
sadAttempt<function (type parameter) T in sadAttempt<T extends object>(arr: T[], key: keyof T): Record<string, T[]>
T extends object>(arr: T[]
arr: function (type parameter) T in sadAttempt<T extends object>(arr: T[], key: keyof T): Record<string, T[]>
T[], key: keyof T
key: keyof function (type parameter) T in sadAttempt<T extends object>(arr: T[], key: keyof T): Record<string, T[]>
T): type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<string, function (type parameter) T in sadAttempt<T extends object>(arr: T[], key: keyof T): Record<string, T[]>
T[]> {
return arr: T[]
arr.Array<T>.reduce<Record<string, T[]>>(callbackfn: (previousValue: Record<string, T[]>, currentValue: T, currentIndex: number, array: T[]) => Record<string, T[]>, initialValue: Record<...>): Record<...> (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((accumulator: Record<string, T[]>
accumulator, val: T extends object
val) => {
const const groupedKey: T[keyof T]
groupedKey = val: T extends object
val[key: keyof T
key];
if (!accumulator[groupedKey]) { accumulator[groupedKey] = []; }
accumulator[groupedKey].push(val: T extends object
val); return accumulator: Record<string, T[]>
accumulator;
}, {} as type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<string, function (type parameter) T in sadAttempt<T extends object>(arr: T[], key: keyof T): Record<string, T[]>
T[]>);
}
The lines with accumulator[groupedKey]
will error with Type 'T[keyof T]' cannot be used to index type 'Record<string, T>'
. Here the keyof T
could be any key in T
so since not every key's value in T
is a string typescript will not let you treat groupedKey
as a string.
We can almost fix this by adding some more information on the input key by binding it to a new generic parameter, though there will still be some issues.
function function betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
betterSadAttempt<function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T extends type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<type PropertyKey = string | number | symbol
PropertyKey, any>, function (type parameter) Key in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
Key extends keyof function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T>(
arr: T[]
arr: function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T[],
key: Key extends keyof T
key: function (type parameter) Key in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
Key
): type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T[function (type parameter) Key in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
Key], function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T[]> {
return arr: T[]
arr.Array<T>.reduce<Record<T[Key], T[]>>(callbackfn: (previousValue: Record<T[Key], T[]>, currentValue: T, currentIndex: number, array: T[]) => Record<T[Key], T[]>, initialValue: Record<...>): Record<...> (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((accumulator: Record<T[Key], T[]>
accumulator, val: T extends Record<PropertyKey, any>
val) => {
const const groupedKey: T[Key]
groupedKey = val: T extends Record<PropertyKey, any>
val[key: Key extends keyof T
key];
if (!accumulator: Record<T[Key], T[]>
accumulator[const groupedKey: T[Key]
groupedKey]) {
accumulator: Record<T[Key], T[]>
accumulator[const groupedKey: T[Key]
groupedKey] = [];
}
accumulator: Record<T[Key], T[]>
accumulator[const groupedKey: T[Key]
groupedKey].Array<T>.push(...items: T[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(val: T extends Record<PropertyKey, any>
val);
return accumulator: Record<T[Key], T[]>
accumulator;
}, {} as type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T[function (type parameter) Key in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
Key], function (type parameter) T in betterSadAttempt<T extends Record<PropertyKey, any>, Key extends keyof T>(arr: T[], key: Key): Record<T[Key], T[]>
T[]>);
}
Here we added a new generic Key extends keyof T
so when we supply a specific key to the function, the Key generic will be narrowed to that value. For example if we did betterSadAttempt(vals, 'someLiteral')
, Key
would exactly be 'someLiteral'
instead of keyof Foo = 'someLiteral' | 'num' | 'object'
However, typescript is still sad on the Record<T[Key], T[]>
lines with Type 'T[Key]' does not satisfy the constraint 'string | number | symbol'
. This error is similar to the error before, basically T[Key]
can not be a key for the Record
since it could be some weird value.
To accomplish this we need to make a helper type that filters down the allowed keys to only keys whose values are string | number | symbol
. We can use a mapped type to do just that
type type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<function (type parameter) T in type MapValuesToKeysIfAllowed<T>
T> = {
[function (type parameter) K
K in keyof function (type parameter) T in type MapValuesToKeysIfAllowed<T>
T]: function (type parameter) T in type MapValuesToKeysIfAllowed<T>
T[function (type parameter) K
K] extends type PropertyKey = string | number | symbol
PropertyKey ? function (type parameter) K
K : never;
};
type type Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T]
Filter<function (type parameter) T in type Filter<T>
T> = type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<function (type parameter) T in type Filter<T>
T>[keyof function (type parameter) T in type Filter<T>
T];
This type helper does a few things. First it maps over all the values in T
([K in keyof T]
) and makes the value the key if it is a subset of string | number | symbol
(T[K] extends PropertyKey ? K
), if it's not a subset its value will be the never
type. Finally, we use an index access type to get all values of the transformed object as a union. This step will drop all the never
values automatically for us since adding never
to a union is like saying or false
its basically is a no op.
That was a mouthful so let's see an example
// from above
interface Foo {
Foo.num: number
num: number;
Foo.someLiteral: "a" | "b" | "c"
someLiteral: 'a' | 'b' | 'c';
Foo.object: Record<string, any>
object: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<string, any>;
}
type type MappedFoo = {
num: "num";
someLiteral: "someLiteral";
object: never;
}
MappedFoo = type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<Foo>;
/*
{
num: "num";
someLiteral: "someLiteral";
object: never;
}
*/
// we replace the values of this object with just the key as a string literal or never
type type FooKeys = "num" | "someLiteral"
FooKeys = type Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T]
Filter<Foo>;
// => "num" | "someLiteral"
// notice the never does not show up in the union
interface AllObjects {
AllObjects.object: Record<string, any>
object: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<string, any>;
AllObjects.diffObject: Record<number, any>
diffObject: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<number, any>;
}
type type MappedAllObjects = {
object: never;
diffObject: never;
}
MappedAllObjects = type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<AllObjects>;
/*
{
object: never;
diffObject: never;
}
*/
type type AllObjectsKeys = never
AllObjectsKeys = type Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T]
Filter<AllObjects>;
// => never
// the output is only never. Think of this like saying "false or false", the output will just be false
With this filter type helper function we can now properly limit the Key
generic by replacing Key extends keyof T
with Key extends Filter<T>
.
Putting it all together
type type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<function (type parameter) T in type MapValuesToKeysIfAllowed<T>
T> = {
[function (type parameter) K
K in keyof function (type parameter) T in type MapValuesToKeysIfAllowed<T>
T]: function (type parameter) T in type MapValuesToKeysIfAllowed<T>
T[function (type parameter) K
K] extends type PropertyKey = string | number | symbol
PropertyKey ? function (type parameter) K
K : never;
};
type type Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T]
Filter<function (type parameter) T in type Filter<T>
T> = type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<function (type parameter) T in type Filter<T>
T>[keyof function (type parameter) T in type Filter<T>
T];
function function groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
groupBy<function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T extends type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<type PropertyKey = string | number | symbol
PropertyKey, any>, function (type parameter) Key in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
Key extends type Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T]
Filter<function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T>>(
arr: T[]
arr: function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T[],
key: Key extends Filter<T>
key: function (type parameter) Key in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
Key
): type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T[function (type parameter) Key in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
Key], function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T[]> {
return arr: T[]
arr.Array<T>.reduce<Record<T[Key], T[]>>(callbackfn: (previousValue: Record<T[Key], T[]>, currentValue: T, currentIndex: number, array: T[]) => Record<T[Key], T[]>, initialValue: Record<...>): Record<...> (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((accumulator: Record<T[Key], T[]>
accumulator, val: T extends Record<PropertyKey, any>
val) => {
const const groupedKey: T[Key]
groupedKey = val: T extends Record<PropertyKey, any>
val[key: Key extends Filter<T>
key];
if (!accumulator: Record<T[Key], T[]>
accumulator[const groupedKey: T[Key]
groupedKey]) {
accumulator: Record<T[Key], T[]>
accumulator[const groupedKey: T[Key]
groupedKey] = [];
}
accumulator: Record<T[Key], T[]>
accumulator[const groupedKey: T[Key]
groupedKey].Array<T>.push(...items: T[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(val: T extends Record<PropertyKey, any>
val);
return accumulator: Record<T[Key], T[]>
accumulator;
}, {} as type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T[function (type parameter) Key in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
Key], function (type parameter) T in groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>(arr: T[], key: Key): Record<T[Key], T[]>
T[]>);
}
const const nums: Record<number, Foo[]>
nums = function groupBy<Foo, "num">(arr: Foo[], key: "num"): Record<number, Foo[]>
groupBy(const vals: Foo[]
vals, 'num');
// nums = Record<number, Foo[]>
const const literals: Record<"a" | "b" | "c", Foo[]>
literals = function groupBy<Foo, "someLiteral">(arr: Foo[], key: "someLiteral"): Record<"a" | "b" | "c", Foo[]>
groupBy(const vals: Foo[]
vals, 'someLiteral');
// literals = Record<"a" | "b" | "c", Foo[]>
const const sad: Record<number | "a" | "b" | "c", Foo[]>
sad = function groupBy<Foo, Filter<Foo>>(arr: Foo[], key: Filter<Foo>): Record<number | "a" | "b" | "c", Foo[]>
groupBy(const vals: Foo[]
vals, 'object');
Now this works great, we can only pass in keys that have valid values, and we even get autocomplete on it! However, one thing that bothers me is the error message in the last case. While it's correct, saying not assignable to parameter of type 'Filter<Foo>'
is not very useful to a user. This pops up sometimes with typescript where it won't show the underlying type and instead just show the higher level type helper instead.
To make the error message show the valid keys we can use a modified version of this "hack". Here instead of creating the Expand
type in the post, we can make our own ValuesOf
to replace the [keyof T]
at the end of Filter
type type ValuesOf<A> = A extends infer O ? A[keyof A] : never
ValuesOf<function (type parameter) A in type ValuesOf<A>
A> = function (type parameter) A in type ValuesOf<A>
A extends infer function (type parameter) O
O ? function (type parameter) A in type ValuesOf<A>
A[keyof function (type parameter) A in type ValuesOf<A>
A] : never;
type type Filter<T> = MapValuesToKeysIfAllowed<T> extends infer O ? (O & MapValuesToKeysIfAllowed<T>)[keyof T | keyof O] : never
Filter<function (type parameter) T in type Filter<T>
T> = type ValuesOf<A> = A extends infer O ? A[keyof A] : never
ValuesOf<type MapValuesToKeysIfAllowed<T> = { [K in keyof T]: T[K] extends PropertyKey ? K : never; }
MapValuesToKeysIfAllowed<function (type parameter) T in type Filter<T>
T>>;
// was Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T]
const const sad: Record<number | "a" | "b" | "c", Foo[]>
sad = function groupBy<Foo, "num" | "someLiteral">(arr: Foo[], key: "num" | "someLiteral"): Record<number | "a" | "b" | "c", Foo[]>
groupBy(const vals: Foo[]
vals, 'object');
Now we have type safety and good error messages!
Possible improvements
One thing this groupBy
function lacks that the lodash groupBy
gives is we do not allow you to pass a function instead of a key to group on. The example in the lodash docs is
const _: _.LoDashStatic
_.LoDashStatic.groupBy<number>(collection: _.List<number> | null | undefined, iteratee?: _.ValueIteratee<number> | undefined): _.Dictionary<number[]> (+1 overload)
Creates an object composed of keys generated from the results of running each element of collection through
iteratee. The corresponding value of each key is an array of the elements responsible for generating the
key. The iteratee is invoked with one argument: (value).groupBy([6.1, 4.2, 6.3], var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.floor(x: number): number
Returns the greatest integer less than or equal to its numeric argument.floor);
// { '4': [4.2], '6': [6.1, 6.3] }
While this is not perfect this mostly works
function function groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
groupByFunc<
function (type parameter) RetType in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
RetType extends type PropertyKey = string | number | symbol
PropertyKey,
function (type parameter) T in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
T // no longer need any requirements on T since the grouper can do w/e it wants
>(arr: T[]
arr: function (type parameter) T in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
T[], mapper: (arg: T) => RetType
mapper: (arg: T
arg: function (type parameter) T in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
T) => function (type parameter) RetType in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
RetType): type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<function (type parameter) RetType in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
RetType, function (type parameter) T in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
T[]> {
return arr: T[]
arr.Array<T>.reduce<Record<RetType, T[]>>(callbackfn: (previousValue: Record<RetType, T[]>, currentValue: T, currentIndex: number, array: T[]) => Record<RetType, T[]>, initialValue: Record<...>): Record<...> (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((accumulator: Record<RetType, T[]>
accumulator, val: T
val) => {
const const groupedKey: RetType extends PropertyKey
groupedKey = mapper: (arg: T) => RetType
mapper(val: T
val);
if (!accumulator: Record<RetType, T[]>
accumulator[const groupedKey: RetType extends PropertyKey
groupedKey]) {
accumulator: Record<RetType, T[]>
accumulator[const groupedKey: RetType extends PropertyKey
groupedKey] = [];
}
accumulator: Record<RetType, T[]>
accumulator[const groupedKey: RetType extends PropertyKey
groupedKey].Array<T>.push(...items: T[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(val: T
val);
return accumulator: Record<RetType, T[]>
accumulator;
}, {} as type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type TRecord<function (type parameter) RetType in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
RetType, function (type parameter) T in groupByFunc<RetType extends PropertyKey, T>(arr: T[], mapper: (arg: T) => RetType): Record<RetType, T[]>
T[]>);
}
const const test: Record<number, number[]>
test = function groupByFunc<number, number>(arr: number[], mapper: (arg: number) => number): Record<number, number[]>
groupByFunc([6.1, 4.2, 6.3], var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.floor(x: number): number
Returns the greatest integer less than or equal to its numeric argument.floor);
// test = Record<PropertyKey, Foo[]>
This works by only letting you pass in functions that return PropertyKey
, and typescript even narrows the types. In this case test
is Record<number, Foo[]>
since TS infers the return type of the grouping function.
If you know how to improve this function further feel free to leave an issue/pr on my blog's GitHub!