Funcție Typescript pentru convertirea unui Array Generic într-un Record cu restricții de tip corespunzătoare


Încerc să scriu o funcție generică care ia un array: T[] și un selector K și returnează un Record<T[K], T> care mapează fiecare element din array de la una dintre proprietățile sale către el însuși.

de ex.

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" } }

Nu reușesc să înțeleg cum să restricționez tipul proprietății selector K astfel încât T[K] să aibă un tip pe care un Record îl acceptă ca și chei, adică string | number | symbol.

Acesta este ceea ce am până acum:

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>
  )
}

Dar asta îmi dă următoarea eroare la 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)

Cum pot să restricționez K astfel încât T[K] să fie string | number | symbol?

Stackblitz

Răspuns #1

Typescript nu știe că valorile lui T sunt valide ca chei.

Tipul 'T[K]' nu satisface constrângerea 'string | number | symbol'.

Aceasta spune că T ar putea fi { a: boolean } sau { a:() => void } și asta ar însemna că T[K] ar putea fi un boolean sau o funcție, niciunul dintre acestea nefiind valid ca nume de proprietate ale unui obiect.

Aș constrânge deci T să fie un obiect cu doar valori care sunt permise să fie chei:

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>)
}

Teren de joacă

Se pare că ai proprietăți care poate nu sunt eligibile ca și chei. Poți în continuare să le tipizezi strict, doar trebuie să știi că dacă valoarea proprietății este un obiect, atunci nu poate fi o cheie, așa că acestea trebuie filtrate. Așa cum s-a menționat anterior, tipul pentru numele unei proprietăți de obiect este string | number | symbol. Așadar, să păstrăm acele valori și să aruncăm restul.

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'

Cu acel tip de ajutor în loc, putem schimba constrângerea lui T în toRecord la:

function toRecord<
  T extends { [P in RecordableKeys<T>]: string | number | symbol },
  K extends RecordableKeys<T>
>

Teren de joacă

Răspunsul #2

Typescript vrea să știe cu siguranță că T este, de fapt, și un 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>)
}