Tipul generic nu este recunoscut corect de Typescript


Avem următorul cod TypeScript:

type Option = {
  name: string;
};

export const filterOptions = <T extends string>(
  options: Record<T, Option>,
): unknown[] =>
  Object.entries(options)
    .map(([key, entry]) => ({
      key,
      ...entry, // <--- Error: Spread types may only be created from object types.
    }))
    .filter(() => {
      // do filtering stuff...
    });

Tipul așteptat pentru entry este Option, dar TS nu recunoaște acest lucru și presupune că este unknown. Problema pare să fie tipul generic T, pentru că schimbarea tipului lui options în Record<string, Option> schimbă tipul lui entry în Option (așa cum era de așteptat).

Iată un link către TS Playground

Ce greșim? De ce nu este recunoscut corect tipul?

Răspuns #1

Tocmai am verificat tipizarea Object.entries():

    /**
     * Returns an array of key/values of the enumerable properties of an object
     * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
     */
    entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];

Opțiunea 1

Se pare că poți să treci tipul argumentului value către Object.entries() în mod explicit. În acest fel recunoaște typings

const filterOptions = <T extends string>(
  options: Record<T, Option>,
) => {

  return Object.entries<Option>(options) // added option type here!
  .map(([key, entry]) => ({
      key,
      ...entry, // It knows for sure now..
  }))
  .filter(() => {
      // do filtering stuff...
  });
}

Opțiunea 2

Tipurile Record folosesc symbols/numbers și strings ca chei.

/**
 * Construct a type with a set of properties K of type T
 */
 type Record<K extends keyof any, T> = {
  [P in K]: T;
};

De exemplu, acesta funcționează:

const d = Symbol('somedescripton')

const a: Record<string, Option> = {
  'a': {name: 'strrst'},
  b: {name:'sdfsdf'},

  0: {name: 'srfsdfds'},
  d: {name: 'sdfsdfd'}
}

Funcția Object.entries() o va converti la chei de tip string, dar tot acceptă și Symbols și numbers!!^Deci, pentru a avea un tip Record cu doar chei de tip string, ar trebui să-l tastezi tu însuți pentru a putea omite conversia explicită:

type StringRecord<T extends string, K> = {[key in keyof T]: K }

const filterOptions = <T extends string>(
  options: StringRecord<T, Option>,
) => {

  return Object.entries(options)
  .map(([key, entry]) => ({
      key,
      ...entry, // works now.. 
  }))
  .filter(() => {
      // do filtering stuff...
  });
}