Sergej
Sergej

Reputation: 1092

Generic type not recognised properly by Typescript

We have following TypeScript code:

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

The expected type of entry is Option, but TS doesn't recognise this and assumes it is unknown. The issue seems to be the generic type T, because changing the type of options to Record<string, Option> changes the type of entry to Option (as expected).

Here is a TS Playground Link

What are we getting wrong? Why is the type not recognised properly?

Upvotes: 3

Views: 2486

Answers (1)

Silvan Bregy
Silvan Bregy

Reputation: 2734

I just checked the Object.entries() typing:

    /**
     * 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][];

Option 1

It appears, you can pass the value argument type to Object.entries() explicitly. This way it regonizes the 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...
  });
}

Option 2

The Record typings make use of symbols / numbers and strings as keys.

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

For example, this does work:

const d = Symbol('somedescripton')

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

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

The Object.entries() will convert it to string keys but it accepts still, Symbols and numbers aswell!!^ So for having a Record type with only string keys you would have it to type yourself for being able to omit the explicit casting:

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

Upvotes: 2

Related Questions