TypeScript: Object.values and Object.entries are unsound and inconsistent with Object.keys.
Search Terms: Object.values Object.entries sound soundness unsound inconsistent Object.keys
Code Proposed change: https://github.com/MicahZoltu/TypeScript/commit/603c36370c7fdcd142b1493ca8cbbb955db73b69
Back in November 2016, a PR (#12207) was submitted to make the types of Object.entries and Object.values generic. This PR was reviewed and accepted, and as a note it was recommended that Object.keys be updated similarly. The author then submitted a PR to update Object.keys (#12253) but @ahejlsberg made the very valid point that this change was unsound and the PR was closed. The author of both PRs then suggested that perhaps #12207 should be reverted, but this revert never happened.
This issue is to discuss options for rectifying this situation and I propose we make Object.entries and Object.values consistent with the sound behavior of Object.keys. The major issue here is that this IS a breaking change since people may be relying on the currently unsound behavior of Object.values and Object.entries. However, I think having TypeScript be inconsistent on this front indefinitely is not good, and I don’t think changing Object.keys to be unsound is the right solution, so at the least this change should be merged into TS 4.x.
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 45
- Comments: 28 (4 by maintainers)
I think if Typescript is about improving developer experience, Object.keys should return
(keyof T)[], it’s easy enough to typecast it to make it bekeyof Tbut, there should at least be something likeObject.typeCastedKeysor something of that ilk.IMO that should be setting off huge alarm bells that the current typing of
Object.keysis a problem. At a minimumas constshould work for objects/records in a similar way as they do for arrays.+1 for moving
Object.entriesandObject.valuesto the currentObject.keysbehavior. Out of the two, it’s the more sound definition and prevents more unexpected type errors. It also aligns more with how Typescript’s type system works, and forces more people to become aware of object type compatibility.Moving
Object.keystokeyof Tprovides a false sense of safety, but also doesn’t align with how Typescript’s type system works. Typescript object types declare subsets of every object, not a strict definition of the object’s shape. It’s important for users to understand that any arbitrary object can be a member of an object type, so long as it has the specified fields, and that when iterating over the object’s keys or values, it’s possible for there to be an infinite number of keys in addition to the specified ones. Accessing properties on an object is safe, but iterating over object properties may produce an unexpected and unsafe result.Typescript is not a language for improving your editor’s autocomplete, it’s a language for preventing type errors. Given how Typescript’s type system works, it’s safer to use an
unknownvalues type and astring[]key type because those are correct. Again, any arbitrary object can be assigned to another type because of how type compatibility works. The alternative not only gives users a false sense of security, but also encourages misconceptions about how Typescript’s type system works.It’s an unfortunate situation but I can’t imagine the upside to moving things in a direction that so many people would simply consider worse, even if it’s more correct. I don’t know if you’ve seen it, but we basically get a bug report or PR to change
Object.keystokeyof Tat least once a week.That is still not type safe. TypeScript has no way to guarantee the fields you can iterate over when you’re iterating over all of an object’s keys. Const assertions prevent type widening, but it’s still possible to set arbitrary fields on them by asserting
anyor something like that. Convenience is not the same thing as type safety.asserting
anykills any type safety. it’s not limited to Object.keys. you can shoot yourself in foot withanyin every possible way.Yeah, I have seen many of the
Object.keysreports and it is a common question in TS Discord, people asking whyObject.keysisn’t generic. What bugs me the most at the moment is the inconsistency betweenObject.keysandObject.values/Object.entries. I would be willing to defer to the TS dev team’s judgement on whether to go with generics or not and I can submit a PR to “fix” either.Personally, I think that the sound solution is preferable, but I understand that often the TS dev team makes soundness sacrifices for usability reasons. The main reason I didn’t lead with a PR to “fix”
Object.keysis because my searching suggests that many have tried this in the past and it is consistently rejected, which is why I thought perhaps going the other direction would get more traction.Aha, I found it finally! I’m having hard enough time justifying it to myself and explaining to my team mates why
Object.keysreturnsstring[]. And once I manage that, I have zero explanation why the same doesn’t apply toObject.values. Even though the same code example that demonstrates soundness ofObject.keysdemonstrates the unsoundness ofObject.values(or vice versa - depending on your view point).I personally do not agree the behavior of
Object.keysis sound - after all there are quite a lot of other ways to break static type guarantees (@susisu just demonstrated one of them in the comment above). But no matter what, I believe consistency is even more important.If adding an extra package (maybe already been installed) to your prject is not a problem, you can have a solution with rambda
toPairs:All Keys types are have been stored!
Docs:
https://ramdajs.com/docs/#toPairs
https://github.com/ramda/ramda/blob/v0.29.0/source/toPairsIn.js
I wrote out this appeal just over a year ago, which unfortunately was closed: https://github.com/microsoft/TypeScript/issues/45835
I feel that we should still consider it, the crux being:
Within a structural type-system and in the absence of Exact Types, it would appear that most Object reflection methods must be typed imprecisely wide, including
Object.entriesandObject.values.But this kind of feels like it violates Typescripts third non-goal: Precise types for object methods are incredibly useful, which is clear by the number of requests, questions and PRs around
Object.keystype signature.Given the state of things it feels like one of two things should happen:
Object.keystype definition override, and maintain at least the same level of unsoundness that already exists.Sample Code
Something like this maintains parity with the status-quo:
Which is not a valid program in the SO example, but demonstrates the same level of unsoundness that already exists
The first part is just a bit of context for why I think one of the two above things should happen
`@jcalz` has enumerated a non-exhaustive list [here](https://github.com/microsoft/TypeScript/issues/45390#issuecomment-895661910), the issue template links to the [SO Answer](https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript). I'm aware of these resources, I know that this comes up quite frequently, but it would be greatly appreciated if you would hear me out :pray:
It’s instructive to go way back to 2016 with this comment in a follow-up PR to #12207 – which will be relevant in a few seconds.
@RyanCavanaughsort of codifies these hesitations in the SO answer. The crux of this argument is that a more-precisely typedObject.keysis potentially unsound. We get a first hand demonstration of this by observing some well-typed program that would produce runtime errors.But TS is already in this state where very similar code, is A) well-typed, but B) will produce runtime errors. This comes down to how
Object.valuesandObject.entriesare typed.See two examples here, using
Object.values:Note: this was known way back in 2016, where
@ahejlsbergcalled this out in the same comment:@amakhrov it’s not just about literals. The same problem occurs any time you have a subtype with additional properties (e.g. here’s an example with classes).
But you’re right that the lack of exactness is implicated here—TypeScript types only ever describe a subset of the properties which will exist at runtime, so the callback given to
Object.keysreceives arguments that may be wider thankeyof T. I’m hopeful that if the JavaScript “Record & Tuple” proposal is implemented it’ll provide some avenue for handling exactness, but time will tell.@ahejlsberg said
what about allow something like (keyof Base | string)[]? so everyone is happy (safety and autocomplete) (maybe typescript keeping some information about unions)
Any solutions provided so far?
@probablykasper This example only shows that
Recordis unsound, not “object keys and values” in general. I assume thatRecordbeing unsound is a design decision, because if you think about it there should be no way to make any element of typeRecord<string, string>(except with black magic like proxies). It has in any case nothing to do withObject.keysandObject.values.An even more minimal example is
This reminds me of variance issues with generics as well
@devinrhode2
yup i know 😃 that’s the part of what about allow and
maybe typescript keeping some information about unionsinstead of “swallow” the other type.related to: https://github.com/microsoft/TypeScript/issues/46859 and https://github.com/microsoft/TypeScript/issues/46548
Honestly, I don’t understand this Object.keys situation. I don’t recall seeing one good example of how typescript will mislead developers by using keyof T.
I do have an npm package which gives you a well-typed alias. I have a stack overflow answer that details an npm package I created,
object-typed: https://stackoverflow.com/a/65117465/565877If we are concerned about actual runtime type checking, TS team should develop a tool to actually check runtime types (during development only)