I'm looking for a generic and type-safe way to model the following JavaScript in TypeScript:
const records = [
{ name: "foo", id: 1, data: ["foo"] },
{ name: "bar", id: 2, data: ["bar"] },
{ name: "baz", id: 3, data: ["baz"] }
];
function keyBy(collection, k1, k2) {
if (k2) {
return collection.reduce((acc, curr) =>
({ ...acc, [curr[k1]]: curr[k2] }), {});
} else {
return collection.reduce((acc, curr) =>
({ ...acc, [curr[k1]]: curr }), {});
}
}
console.log(keyBy(records, "name", "data"));
// { foo: [ 'foo' ], bar: [ 'bar' ], baz: [ 'baz' ] }
console.log(keyBy(records, "name"));
// {
// foo: { name: 'foo', id: 1, data: [ 'foo' ] },
// bar: { name: 'bar', id: 2, data: [ 'bar' ] },
// baz: { name: 'baz', id: 3, data: [ 'baz' ] }
// }
The idea is to create a util that will reduce an array into an object keyed by the value at a given key, and with a value of either the entire object, or optionally a specific data point at a given second key (this explanation may be a bit poor, but hopefully the example speaks for itself).
This is pretty simple JS, but seems hard to get the types right in TS. Here's what I've come up with so far, but I've needed to create two functions in order to get the return types right and if all feels a bit hacky. I was unable to get a conditional return type to work here, so am OK with two functions if that's the way it has to be, but wondering if there's a better approach here (perhaps something that could result in Record<T[K], T>
or Record<T[K], T[K2]>
rather than the record being keyed by ObjectKey
). Thanks.
type ObjectKey = string | number | symbol;
const isValidKey = (x: any): x is ObjectKey =>
typeof x === "string" || typeof x === "number" || typeof x === "symbol";
function keyBy<T extends object, K extends keyof T>(collection: T[], key: K) {
return collection.reduce((acc, curr) => {
const valueAtKey = curr[key];
if (isValidKey(valueAtKey)) {
return { ...acc, [valueAtKey]: curr };
}
throw new Error("T[K] is not a valid object key type");
}, {} as Record<KeyType, T>);
}
function keyByWith<T extends object, K extends keyof T, K2 extends keyof T>(
collection: T[],
k: K,
k2: K2,
) {
return collection.reduce((acc, curr) => {
const valueAtKey = curr[k];
if (isValidKey(valueAtKey)) {
return { ...acc, [valueAtKey]: curr[k2] };
}
throw new Error("T[K] is not a valid object key type");
}, {} as Record<ObjectKey, T[K2]>);
}
P.S. I know lodash has a similar keyBy
function, but I don't think they have anything similar to keyByWith
shown above.