Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object.entries correct typings #35101

Closed
3 of 7 tasks
tcharlat opened this issue Nov 14, 2019 · 9 comments
Closed
3 of 7 tasks

Object.entries correct typings #35101

tcharlat opened this issue Nov 14, 2019 · 9 comments
Labels
Duplicate An existing issue was already created

Comments

@tcharlat
Copy link

Search Terms

Object.entries

Potentially related issues:

#32771

Suggestion

Typings of Object.entries() returned [key, value] tupple array is very weak and infer poorly from argument, I think it would be better to implement more relevant ones in lib.es2017.object.d.ts

Use Cases

When iterating on an object entries array returned by Object.entries, we need to manually type cast the array to avoid false positive and false negative typings shortcomings.

const a = Object.entries({foo: "bar", baz: 0}); // [string, string | number][]
const b = Object.entries({foo: "bar", baz: 0} as const); // [string, 0 | "bar"][]

It is not possible to infer typings in .mapor .forEach

a.forEach(entry => {
  if(entry[0] === "foo") {
    let value = entry[1]; // string | number
    entry[1].toUpperCase(); // error (false positive)
  } else if (entry[0] === "baz") {
    let value = entry[1]; // string | number
  } else if (entry[0] === "invalid") { // no error (false negative)
    // ...
  }
})

b.forEach(entry => {
  if(entry[0] === "foo") {
    let value = entry[1]; // 0 | "bar"
  }
})

Examples

We can easily write our own entries function:

type Entries<T> = {
  [K in keyof T]: [K, T[K]]
}[keyof T][]

function entries<T>(obj: T): Entries<T> {
  return Object.entries(obj) as any;
}

Wich gives me an array of tupples with inferables key/value:

const c = entries({foo: "bar", baz: 0}); // (["foo": string] | ["baz": number])[]
const d = entries({foo: "bar", baz: 0} as const); // (["foo": "bar"] | ["baz": 0])[]


c.forEach(entry => {
  if (entry[0] === "foo") {
    let value = entry[1]; // string
    entry[1].toUpperCase(); // no error
  } else if (entry[0] === "baz") {
    let value = entry[1]; // number
  }
  if (entry[0] === "invalid") { // error: "baz" and "invalid" have no overlap
    // ...
  }
});

d.forEach(entry => {
  if (entry[0] === "foo") {
    let value = entry[1]; // "bar"
  } else if (entry[0] === "baz") {
    let value = entry[1]; // 0
  }
  if(entry[1] === "bar") {
    let key = entry[0]; // "foo"
  }
});

We may implement a better default behaviour for Object.entries in the lib:

  /**
   * 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: T): T extends ArrayLike<infer U> ? [string, U][] : { [K in keyof T]: [K, T[K]] }[keyof T][];

The same concepts can be applied for better typings of Object.values

/**
   * Returns an array of 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.
   */
  values<T>(o: T): T extends ArrayLike<infer U> ? U[] : (T[keyof T])[];

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
  • This wouldn't be a breaking change in existing TypeScript/JavaScript code

I need some feedback to identify potential breaking changes.
I think it 'may' induce breaking change since it's reducing typecast necessity. If manual casts are incompatible with the current feature request, there may be a need to remove cast or force through an intermediary any.

I tested it in a project of mine and had no issue since the existing cast was correctly made with the same source of truth as the Object.values argument type.

I can't say I'm sure it does. Feedback would be appreciated.

@MartinJohns
Copy link
Contributor

MartinJohns commented Nov 14, 2019

Duplicate of #20322. The issue boils down to: #12253 (comment)

@fatcerberus
Copy link

Also see https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript for rationale.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Nov 14, 2019
@tcharlat
Copy link
Author

Hello @MartinJohns @fatcerberus and @RyanCavanaugh
I red previous issues, the 2016 Anders comment and the stackoverflow answer.

I am not convinced though, I thinks some assumptions can be challenged in regards of the current state of typescript.

In particular, contravariance issues are widespread and don't always lead to any fallback.

Would you mind refreshing this topic and allowing me to advocate another opinion ?

@RyanCavanaugh
Copy link
Member

If you think there's some solution available, please do describe

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@Pzixel
Copy link

Pzixel commented Jan 11, 2022

So any further work on this? Okay, if object can hold more properties than keyof T then typesystem should state that it returns at least these properties and maybe some more. While it would require to handle defualt cases this code would always typecheck without additional casts:

const source: SomeType = ....;
const result: SomeType  = Object.assign({}, ...Object.entries(source).map(([k, v]) => ({[k]: v})));

While it may process some extra properties that aren't part of SomeType it still says that entries contain at least keyof SomeType values, which is enough to construct a valid SomeType instance

Wouldn't it be nicer to have it like something about this?

keys<T>(obj: T): ((keyof T)[] & string[]);

So we don't know what elements of array are but we know for sure it has keys of T.

I strongly believe that this code

const obj = {A: 10};
const keys = Object.keys(obj);
if (keys.includes("A")) {
  console.log("Hello");
}

Should generate a warning "condition is always true". And it's true even if Object.keys returned as many other elements as it likes.

@Pzixel
Copy link

Pzixel commented Jan 11, 2022

I've also found a great answer about how to restrict object from having extra properties. This may help as well: https://stackoverflow.com/questions/49580725/is-it-possible-to-restrict-typescript-object-to-contain-only-properties-defined

@exelimpichment
Copy link

exelimpichment commented Nov 25, 2023

i had to do smth like this:

 {Object.entries(navigation).map(([key, value]) => (
              <DropdownItem
                key={key}
                item={{ pathname, lang, key, value }}
              />
            ))}

and as you might guess key, value was not inferred properly

so I did something like this:

type Entry<T> = [keyof T, T[keyof T]]

  function objectEntries<T extends object>(obj: T): Entry<T>[] {
    return (Object.keys(obj) as Array<keyof T>).map((key) => [
      key,
      obj[key],
    ]) as Entry<T>[];
  }

and transform the first part to:

    {objectEntries(navigation).map(([key, value]) => (
              <DropdownItem key={key} item={{ pathname, lang, key, value }} />
            ))}

everything is typed OK now

@Vipigal
Copy link

Vipigal commented Jan 10, 2024

i had to do smth like this:

 {Object.entries(navigation).map(([key, value]) => (
              <DropdownItem
                key={key}
                item={{ pathname, lang, key, value }}
              />
            ))}

and as you might guess key, value was not inferred properly

so I did something like this:

type Entry<T> = [keyof T, T[keyof T]]

  function objectEntries<T extends object>(obj: T): Entry<T>[] {
    return (Object.keys(obj) as Array<keyof T>).map((key) => [
      key,
      obj[key],
    ]) as Entry<T>[];
  }

and transform the first part to:

    {objectEntries(navigation).map(([key, value]) => (
              <DropdownItem key={key} item={{ pathname, lang, key, value }} />
            ))}

everything is typed OK now

worked for me on TS 4.8.4!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

8 participants