Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript Generic Array to Record function with proper type restrictions

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

like image 647
frido Avatar asked Jul 16 '26 03:07

frido


2 Answers

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

like image 71
Alex Wayne Avatar answered Jul 18 '26 17:07

Alex Wayne


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>)
}
like image 44
Evert Avatar answered Jul 18 '26 17:07

Evert



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!