2

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?

4
  • You need just provide Object.entries with explicit generic argument like here: tsplay.dev/mMVjzW . Let me know if it wirks for you Commented Nov 30, 2021 at 10:09
  • Thanks, this works :-) But it still feels a bit like a workaround... I don't understand why the type can be inferred without generics, but can't be inferred with generics ¯_(ツ)_/¯ Commented Nov 30, 2021 at 11:58
  • 1
    What is the purpose of your type parameter T? It only occurs in one parameter and not the return type, so your function would have essentially the same signature if you just deleted it and replaced it with string. Commented Nov 30, 2021 at 12:14
  • This is just a function stub to outline the issue, the generic type is part of the return type in the end and helps to have a better type safety in the code, which uses this function. Commented Dec 1, 2021 at 9:30

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

3 Comments

This answer doesn't help... sorry... The Generics are on purpose, just deleting the generics wouldn't make it any better
Updated my answer, just giving you my thoughts^^
The update is very helpful, Thank you. Will accept this

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.