Issue
I wanted to write a generic function that can create a map of objects where the keys are properties of the objects themselves, as such grouping them by certain property values. Of course, this only works property if those property values are unique for every object, such as database IDs.
I ended up writing the following code:
type PartialRecord<K extends keyof any, T> = {
[P in K]?: T;
};
export class CustomMaps {
public static createInnerMap<T, K extends keyof any>(
array: T[], customKey: keyof T
): PartialRecord<K, T> {
const innerMap: PartialRecord<K, T> = {};
for (let i = 0; i < array.length; ++i) {
innerMap[array[i][customKey] as K] = array[i];
}
return innerMap;
}
}
It gives error: Conversion of type 'T[keyof T]' to type 'K' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
What it should do is this:
class Car {
constructor(readonly color: string, readonly wheelcount: number) {}
}
const redCar = new Car('red', 4);
const blueCar = new Car('blue', 8);
const colorMap = CustomMaps.createInnerMap<Car, string>([redCar, blueCar, yellowCar], 'color');
colorMap['red']; // should return redCar instance
const wheelMap = CustomMaps.createInnerMap<Car, number>([redCar, blueCar, yellowCar], 'wheelcount');
wheelMap[8] // should return blueCar instance
Without casting innerMap[array[i][customKey]] to unknown, how can I ensure that array[i][customKey] will return a type that can be used for indexing (specifically the type I substitute for K)?
Solution
It would appear I have found the solution without having to use the dirty fix of casting "as unknown as K".
I made these custom types:
type TRecordKey = string | number | symbol;
type TRecurringRecord<K extends T[keyof T] & TRecordKey, T> = {
[P in K]?: T
}
Then I adjusted the above code like this:
public static recurringMap<K extends T[keyof T] & TRecordKey, T>(
array: T[],
customKey: keyof T
): TRecurringRecord<K, T> {
const recurringMap: TRecurringRecord<K, T> = {};
for (let i = 0; i < array.length; ++i) {
recurringMap[array[i][customKey] as K] = array[i];
}
return recurringMap;
}
Now we have a generic function that creates a map of objects where the keys of that map are a property of the object itself.
EDIT 03-10-2022
I wrote simpler code to achieve basically the same thing, as the code above contains a lot of superfluous statements.
- Replace custom types TRecordKey and TRecurringRecord with an Indexable type, which just states that keys should be strings. This is a much simpler way to take care of the "neither type sufficiently overlaps with the other" warning. (also numbers and symbols are no longer accepted as property names).
- No more need to cast "as K".
- Map to arrays of type T instead of only 1 instance.
- Use Reduce function for creating a map instead of a for-loop.
type TIndexable = {
[key: string]: any;
};
const mapFromProperty = <T extends TIndexable>(
objects: T[], propertyName: keyof T
): Record<string, T[]> => {
return objects.reduce((accumulator: Record<string, T[]>, current: T): Record<string, T[]> => {
if (!accumulator[current[propertyName]]) {
accumulator[current[propertyName]] = [current];
} else {
accumulator[current[propertyName]].push(current);
}
return accumulator;
}, {});
}
Usage:
export class CuteKitty {
constructor(readonly eyeColor: string, readonly furColor: string){}
}
const redKitty = new CuteKitty('blue', 'red');
const blueKitty = new CuteKitty('blue', 'blue');
const eyeColorMap = mapFromProperty([redKitty, blueKitty], 'eyeColor');
console.log(eyeColorMap['red']); // Logs undefined
console.log(eyeColorMap['blue']); // Logs [redKitty, blueKitty]
const furColorMap = mapFromProperty([redKitty, blueKitty], 'furColor');
console.log(furColorMap['red']); // Logs [redKitty]
console.log(furColorMap['blue']); // Logs [blueKitty]
Answered By - CoderApprentice
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.