microsoft/TypeScript

Improve Object.keys(object) and Object.values(object)

KSXGitHub opened this issue · 5 comments

Search Terms

Suggestion

When all keys and values of object is known

  • Object.keys(object) should return array type of union of known properties
  • Object.values(object) should return array type of union of known values

How to implement?

I tried creating the 2 functions:

declare function keys<
    Object extends { [_: string]: any },
    Key extends keyof Object
> (obj: Object): Key[]

declare function values<
    Object extends { [_: string]: any },
    Key extends keyof Object
> (obj: Object): Object[Key][]

I also tested them: playground link

Use Cases

Use with objects

const object = {
  abc: 123 as 123,
  def: 456 as 456
}

const objectKeys = Object.keys(object) // objectKeys: Array<'abc'|'def'>
const objectVals = Object.values(object) // objectVals: Array<123|456>

Use with enums

enum Foo {
  abc,
  def = 'def'
}

const FooKeys = Object.keys(Foo) // FooKeys: Array<'abc' | 'def'>
const FooVals = Object.values(Foo) // FooVals: Foo[]

Caveats

It is possible to ignore extra keys/values that don't exist in type definition

interface Foo {
  abc: 'abc' // the only known key/value pair of Foo
}

const bar = {
  abc: 'abc' as 'abc', // exist in Foo
  ghi: 'ghi' as 'ghi' // doesn't exist in Foo
}

const foo: Foo = bar
const keys = Object.keys(foo) // TS emits `keys: 'abc'[]` but the correct value is `['abc', 'def']`

Examples

See use cases and playground link above

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. new expression-level syntax)

Duplicate of this closed PR #25832.

As an example why:

const object = {
  abc: 123 as 123,
  def: 456 as 456
}

const objectKeys = Object.keys<{abc: 123}, 'abc'>(object) // objectKeys: Array<'abc'> whoops, array will contain 'def'

enum Foo {
abc,
def = 'def'
}

const FooKeys = Object.keys(Foo) // FooKeys: Array<'abc' | 'def'>
const FooVals = Object.values(Foo) // FooVals: Foo[]

keys of enum has not only "key"

While crawling through the issues I found this merged PR: #18175
Based on this precedence, would it make sense to type Object.keys as the union of all known properties and string[] for the unknown ones? Or is there simply no reasonable way to represent such a type?

@AlCalzone I think there is simply no way to represent this safely. Subtyping and this kind of reflection are incompatible because there is no correspondence between the value and the typescript type.

The reason getOwnPropertyDescriptors gets away with it is because you are immediately using the synthesised keys, and in the event the key doesn't exist, you can return a safe default value (PropertyDescriptor).

Dates back to at least #12253