I'm trying to write a generic function that takes an array: T[] and a selector K and returns a Record<T[K], T> that maps each element in the array from one of its properties to itself.
e.g.
const input1 = [{ name: "Alice" }, { name: "Bob" }];
const output1: Record<string, { name: string }> = toRecord(input1, "name");
// { Alice: { name: "Alice" }, Bob: { name: "Bob" } }
// -----------------------------------------------------------------
enum Name {
Alice = "Alice",
Bob = "Bob"
}
const input2 = [{ name: Name.Alice }, { name: Name.Bob }];
const output2: Record<Name, { name: Name }> = toRecord(input2, "name");
// { Alice: { name: "Alice" }, Bob: { name: "Bob" } }
// -----------------------------------------------------------------
interface Person {
id: number;
name: string;
sibling?: Person;
}
const input3: Person[] = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
const output3: Record<number, Person> = toRecord(input3, "id");
// { 1: { id: 1, name: "Alice" }, 2: { id: 2, name: "Bob" } }
I can't figure out how to restrict the type of the selector property K so that T[K] has a type a Record accepts as keys i.e. string | number | symbol.
This is what I have so far:
export function toRecord<T, K extends keyof T>(
array: T[],
selector: K
): Record<T[K], T> {
return array.reduce(
(acc, item) => (acc[item[selector]] = item, acc),
{} as Record<T[K], T>
)
}
But this gives me the following error at T[K]:
Type 'T[K]' does not satisfy the constraint 'string | number | symbol'. Type 'T[keyof T]' is not assignable to type 'string | number | symbol'. Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string | number | symbol'. Type 'T[string]' is not assignable to type 'string | number | symbol'. Type 'T[string]' is not assignable to type 'symbol'. Type 'T[keyof T]' is not assignable to type 'symbol'. Type 'T[K]' is not assignable to type 'symbol'. Type 'T[keyof T]' is not assignable to type 'symbol'. Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'symbol'. Type 'T[string]' is not assignable to type 'symbol'.(2344)
How can I restrict K so that T[K] is string | number | symbol?
Stackblitz
Typescript doesn't know that the values of T are valid as keys.
Type 'T[K]' does not satisfy the constraint 'string | number | symbol'.
This is saying that T could be { a: boolean } or { a: () => void } and that would mean that T[K] could be a boolean or a function, neither of which are valid as object property names.
So I would constrain T to be an object with only values that are allowed to be keys:
function toRecord<
T extends { [K in keyof T]: string | number | symbol }, // added constraint
K extends keyof T
>(array: T[], selector: K): Record<T[K], T> {
return array.reduce((acc, item) => (acc[item[selector]] = item, acc), {} as Record<T[K], T>)
}
Playground
It sounds like you have a properties which may not be eligible as keys. You can still strongly type that, just know that if the value of the property is an object, then it can't be a key, so those must be filtered out. As previously states, the type for an object property name is string | number | symbol. So lets keep those values, and discard the rest.
type RecordableKeys<T> = {
// for each key in T
[K in keyof T]:
// is the value a valid object key?
T[K] extends string | number | symbol
// Yes, return the key itself
? K
// No. Return `never`
: never
}[keyof T] // Get a union of the values that are not `never`.
interface Person {
name: string,
age: number
sibling?: Person
}
type PersonRecordableKeys = RecordableKeys<Person> // 'name' | 'age'
With that helper type in place, we can change the constraint of T in toRecord to:
function toRecord<
T extends { [P in RecordableKeys<T>]: string | number | symbol },
K extends RecordableKeys<T>
>
Playground
Typescript wants to know for sure that T is actually also a record:
export function toRecord<T extends Record<string, any>, K extends keyof T>(array: T[], selector: K): Record<T[K], T> {
return array.reduce((acc, item) => ({ ...acc, [item[selector]]: item }), {} as Record<T[K], T>)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With