GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Object.keys() types refinement, and Object.entries() types bugfix by ethanresnick · Pull Request #12253 · microsoft/TypeScript

Conversation

@ethanresnick

@ethanresnick Ethan Resnick (ethanresnick) commented Nov 15, 2016 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Per Mohamed Hegazy (@mhegazy)'s suggestion in #12207, this pr applies the logic from that PR to Object.keys.

It also fixes a bug in that PR (namely, supporting Object.entries([/* some array */])) and adds a test case for that. Finally, it requires that the keys in Object.keys and Object.entries entries be strings, even though the keyof operator can return string | number. (That part of the PR is now covered by #12425.)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Ethan Resnick (ethanresnick) added 2 commits November 14, 2016 21:17
@ethanresnick
Object.entries() types: fixup
5599d05
1. Special case array. Because array instances are typed as
implementing an interface that lists all their public methods, `keyof
Array` returns all those method names as keys…even though they’re not
actually enumerable at runtime.

2. Intersect keyof T with string, because `keyof T` can return `number
| string` in some cases, whereas the entries’ keys will always be
strings.
@ethanresnick
Object.keys: more precise types
cda0c4a
Applying the same logic used on Object.entries in the prior commit and
in microsoft#12207
@msftclas

Microsoft Contribution License Agreements (msftclas) commented Nov 15, 2016

Copy link
Copy Markdown

Hi Ethan Resnick (@ethanresnick), I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by Microsoft and real humans are currently evaluating your PR.

TTYL, MSBOT;

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@msftclas Microsoft Contribution License Agreements (msftclas) added the cla-already-signed label Nov 15, 2016
mhegazy
Mohamed Hegazy (mhegazy) suggested changes Nov 15, 2016
View reviewed changes
Comment thread src/lib/es5.d.ts Outdated
* @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.
*/
keys<T>(o: Array<T>): string[];
keys<T extends { [key: string]: any }>(o: T): (keyof T & string)[];

@mhegazy Mohamed Hegazy (mhegazy) Nov 15, 2016

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key of T & string is just string. so this does not help you much.

keys<T>(o: Array<T>): string[];
keys<T>(o: T): (keyof T)[];

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@ethanresnick Ethan Resnick (ethanresnick) Nov 15, 2016 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mohamed Hegazy (@mhegazy) Edit: sorry, I misread your comment.

key of T & string is often ("key1" & string) | ("key2" & string) | ... | ("keyn" & string), not just string.

The intersection is needed because sometimes keyof T is string | number, and we want to exclude number as a possible type for the entry keys. See this test case: https://github.com/Microsoft/TypeScript/pull/12253/files#diff-3ed8d911aae864ffc1d88e62bcb8dc47R15

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

siri4 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

@ethanresnick Ethan Resnick (ethanresnick) Nov 15, 2016

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These intersections do make the intellisense popups uglier, but they're required for correctness.

Could the solution to that ugliness be for the compiler to do some work simplifying intersections (and unions)? E.g., 'a' & string could just be simplified to to 'a' (or, more generally, type & subType could just be simplified to subType). Any efforts in that direction could also help with the bug I brought up in #11426. Would this be that hard? I'd kind of expect simplifying logical formulations to be a somewhat solved problem with theorem provers etc, though I haven't looked into it, so maybe that's incredibly naive. Or maybe it's way too slow to apply here in the general case. Still, specific simplification rules might be a good idea.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@mhegazy Mohamed Hegazy (mhegazy) Nov 15, 2016

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rather leave it string[] then. adding the & string removes any type safety the literal types give you.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@ethanresnick Ethan Resnick (ethanresnick) Nov 16, 2016 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding the & string removes any type safety the literal types give you.

I don't really understand that. Why would you lose type safety? The & string version still only allows certain strings, rather than all strings, which is more safe.

Let me give the real code example that motivated these two PRs:

const loggers = {
  info: ...,
  warn: ...,
  error: ...
}

Object.entries(loggers).forEach(([name, logger]) => {
  // In here, I want typeof name to be "info" | "warn" | "error", so that 
  // the line below will fail if I add a key to loggers by mistake that
  // doesn't have a corresponding method on console. (This in fact
  // happened: I originally had a debug logger, but only later realized
  // that there is no console.debug() on the node console object.)

  logger.log = console[name].bind(console);
})

To make that code work, string[] isn't enough for typeof name. The change merged in #12207 meanwhile, works perfectly, but fails in the somewhat obscure case that motivated the & string addition in this PR. Using & string, the type of name is inferred as ("info" & string) | ("warn" & string) | ("error" & string). That should, if I understand correctly, be equivalent to "info" | "warn" | "error". However, I instead get an error. That seems like a separate bug, though (opened issue #12289 for it), also tied to simplification.

Going forward... if #12289 were addressed, I think the & string would be the right type signature for Object.keys/entries. But, if #12289 can't be easily addressed, maybe it's better to ignore case that & string was meant to solve, rather than giving up on having key-named literals entirely. If we did that, the signature would be:

keys<T>(o: Array<T>): string[];
keys<T extends { [key: string]: any }>(o: T): (keyof T)[];
keys(o: any): string[];

Then, Object.keys(loggers) would correctly be ("info" | "warn" | "error")[], but let y: { [x: string]: any } = {}; Object.keys(y) would be (string | number)[] rather than string[]. Frankly, that sounds like a net win.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@ethanresnick Ethan Resnick (ethanresnick) mentioned this pull request Nov 16, 2016
Unsimpilifed Interesction Types Prevent Type Inference #12289
Closed
Ethan Resnick (ethanresnick) added 2 commits November 27, 2016 01:10
@ethanresnick
Merge branch 'master' into object-keys-types
944625a
@ethanresnick
Object.keys/entries: remove keyof & string intersections
29105a6
Now that keyof T only returns string or string subtypes (microsoft#12425),
there’s no need to do keyof T & string.
@ethanresnick

Ethan Resnick (ethanresnick) commented Nov 27, 2016

Copy link
Copy Markdown
Contributor Author

Mohamed Hegazy (@mhegazy) I've updated this PR to replace keyof T & string with simply keyof T, as the string intersections are no longer necessary now that keyof always returns a string (from #12425). So this should be good to go.

Cc Anders Hejlsberg (@ahejlsberg): you expressed skepticism about typing Object.keys with keyof in #12289 (comment). But if/now that for in is typed with keyof (#12314), it seems like this makes sense, no?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented Nov 27, 2016

Copy link
Copy Markdown
Member

Ethan Resnick (@ethanresnick) My reservations about Object.keys returning (keyof T)[] still hold. It makes sense only in the domain of type variables (i.e. T and keyof T). Once you move to the instantiated type world it degenerates because an object can (and often does) have more properties at run-time than are statically known at compile time. For example, imagine a type Base with just a few properties and a family of types derived from that. Calling Object.keys with a Base would return a (keyof Base)[] which is almost certainly wrong because an actual instance would be a derived object with more keys. It completely degenerates for type {} which could be any object but would return never[] for its keys.

BTW, I have the same reservations about Object.entries which I notice is now typed this way.

Note that the changes we made to for-in specifically apply only when objects have a type parameter type. In for (let x in obj) ... where obj is of type T, if T is a type parameter we infer keyof T for x, but otherwise we infer type string. To do the same for Object.keys would require some way to constrain a type argument to only be type parameter, which currently isn't possible.

raybooysen, pelotom, SlurpTheo, sgoll, s-ve, bcherny, NotIntMan, and nickserv reacted with thumbs up emoji thorjarhun and fr0 reacted with thumbs down emoji frederickfogerty, Deilan, cevek, thorjarhun, and c69 reacted with confused emoji
All reactions
  • 👍 8 reactions
  • 👎 2 reactions
  • 😕 5 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ethanresnick

Ethan Resnick (ethanresnick) commented Nov 28, 2016

Copy link
Copy Markdown
Contributor Author

Anders Hejlsberg (@ahejlsberg) What you're saying makes sense, assuming I'm understanding it correctly, makes sense. I'll close this PR then. Feel free to revert #12207 as well.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ethanresnick Ethan Resnick (ethanresnick) closed this Nov 28, 2016
@mhegazy Mohamed Hegazy (mhegazy) mentioned this pull request Nov 29, 2016
Revert https://github.com/Microsoft/TypeScript/pull/12207 #12547
Merged
@jwbay jwbay mentioned this pull request Jan 2, 2017
Object.keys to use keyof #13254
Closed
@mhegazy Mohamed Hegazy (mhegazy) mentioned this pull request Apr 24, 2017
Add Object.getOwnPropertyDescriptors definition #15280
Closed
@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) mentioned this pull request May 3, 2017
Suggestion: Object.getOwnPropertyNames and Object.keys should return type with keyof #15570
Closed
@maiermic Michael Maier (maiermic) mentioned this pull request May 6, 2017
number should be subtype of keyof T[] #13715
Closed
@hediet Henning Dieterichs (hediet) mentioned this pull request May 6, 2017
Object.keys does not return strongly typed keys in es6 #15627
Closed
@jwbay jwbay mentioned this pull request Jul 18, 2017
Update type definition of Object.keys #17267
Closed
@mhegazy Mohamed Hegazy (mhegazy) mentioned this pull request Sep 6, 2017
Set a type guard for hasOwnProperty signature. #18282
Closed
@Deilan

Michael Ovchinnikov (Deilan) commented Sep 17, 2017

Copy link
Copy Markdown

Anders Hejlsberg (@ahejlsberg) And what about using it on Readonly<T> type?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@dyst5422

Dylan Stewart (dyst5422) commented Sep 26, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

When an object has an index though, shouldn't it return the index type?

const a: {[key: number]: any} = {};
Object.keys(a) // should be a number

Basically with an index, we are saying that the index HAS to be a certain type, so the above concern about dynamic keys doesn't follow.

...unless javascript casts to strings in Object.keys...

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@NN---

NN (NN---) commented Oct 13, 2017

Copy link
Copy Markdown

Dylan Stewart (@dyst5422) Keys are returner as strings.
As well in for..in you get strings.

var a={1:2};
alert(typeof Object.keys(a)[0])

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mohsen1 Mohsen Azimi (mohsen1) mentioned this pull request Oct 24, 2017
lib.d.ts Make Object.keys generic #19448
Closed
@mhegazy Mohamed Hegazy (mhegazy) mentioned this pull request Nov 29, 2017
Object.entries generic definition #20322
Closed
@ccorcos

Chet Corcos (ccorcos) commented Nov 29, 2017

Copy link
Copy Markdown

Anders Hejlsberg (@ahejlsberg) I'm not sure I understand your reasoning. If you're object has type {[key: string]: number} then Object.keys will return string[]. But if you object is a literal type {a: 1, b: 2} then the return type will be Array<"a" | "b">. That makes sense doesnt it?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pelotom

Tom Crockett (pelotom) commented Nov 29, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Chet Corcos (@ccorcos) no, because lots of objects with other extraneous keys can have type { a: 1; b: 2 }. For example,

declare const x: { a: 1, b: 2, c: 3 }
const y: { a: 1; b: 2 } = x

for (const k in Object.keys(y)) {
  // what should be the type of k?
}

k: 'a' | 'b' is wrong, because k will take on the value 'c' as well!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ccorcos

Chet Corcos (ccorcos) commented Nov 30, 2017

Copy link
Copy Markdown

I see. Sounds like that should be a type error then. haha

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@aluanhaddad Aluan Haddad (aluanhaddad) mentioned this pull request Dec 6, 2017
Redefine Object.keys to use keyof #20503
Closed
@aaronbeall

Aaron Beall (aaronbeall) commented Dec 18, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

I understand the reasoning here and it makes sense, but --strict just made this a lot more annoying because now with strictFunctionTypes you can no longer do this:

interface Foo { foo: string; }
declare const foo: Foo

Object.keys(foo).forEach((key: keyof Foo) => { }) // Error

Error: Argument of type '(key: "foo") => void' is not assignable to parameter of type '(value: string, index: number, array: string[]) => void'.

Which is a pain because with noImplicitAny you can't use string as a lookup:

Object.keys(foo).forEach(key => {
  foo[key] // Error: Element implicitly has an 'any' type because type 'Foo' has no index signature.
})

You have to assert key as keyof T to use it as a lookup.

That's pretty annoying. Is there no way we could allow key to be keyof T? (For cases you're sure T only actually has keys of T)? Since AFAIK there's no way to actually check at runtime that key is in keyof T there seems to be no safe way to ensure that's true, so an assertion seems necessary (without bringing in a runtime type system of some kind).

zakjan and ryanberckmans reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@chriskrycho

Chris Krycho (chriskrycho) commented Dec 22, 2017

Copy link
Copy Markdown

Yeah, the only other workaround also ends up being kind of gross to write. You can write it, but it's gross:

(Object.keys(foo) as (keyof Foo)[]).forEach((key) => { ... })

I just found my way here after writing up an issue around that b/c of the really poor ergonomics associated with it. Perhaps this could work if the exact types proposal landed? 🤔

bcherny and rhys-jmc reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pelotom

Tom Crockett (pelotom) commented Dec 22, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

I'm not sure why this is worth debating here. It's really easy to write your own function which does this "at your own risk" if you want it:

export function keys<O>(o: O) {
  return Object.keys(o) as (keyof O)[];
}
matheusMFCosta, dylanpyle, SlurpTheo, Zip753, and JFFilip reacted with thumbs up emoji
All reactions
  • 👍 5 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@chriskrycho

Chris Krycho (chriskrycho) commented Dec 22, 2017

Copy link
Copy Markdown

FWIW, my comment wasn't offered as "debate." I was mostly thinking out loud if there were ways to get both what many users will expect and satisfy the point made up-thread last year. 🙂

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mhegazy Mohamed Hegazy (mhegazy) mentioned this pull request Feb 9, 2018
for(...in...) with mapped types cannot see index type (noImplicitAny) #14451
Closed
@robbiespeed Robbie Speed (robbiespeed) mentioned this pull request Feb 20, 2018
keyof any incorrectly maps (only) to type string #21983
Closed
@ghost ghost mentioned this pull request Feb 20, 2018
The usage of the term "key" is ambiguous in relation to object properties tc39/ecma262#1110
Closed
@j-oliveras Jordi Oliveras Rovira (j-oliveras) mentioned this pull request Mar 21, 2018
Fix Object.keys definiton type #22777
Closed
@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) mentioned this pull request Apr 19, 2018
Empty type inferred by Object.entries #21826
Open
@domoritz

Dominik Moritz (domoritz) commented Jun 1, 2018

Copy link
Copy Markdown

I believe the updated version for 2.9 is

export const keys = Object.keys as <T>(o: T) => (Extract<keyof T, string>)[];
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@domoritz Dominik Moritz (domoritz) mentioned this pull request Jun 1, 2018
keyof behavior in 2.9 #24574
Closed
@microsoft Microsoft (microsoft) locked and limited conversation to collaborators Aug 2, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

1 more reviewer
@mhegazy Mohamed Hegazy (mhegazy) mhegazy requested changes
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

12 participants

@ethanresnick @msftclas @ahejlsberg @Deilan @dyst5422 @NN--- @ccorcos @pelotom @aaronbeall @chriskrycho @domoritz @mhegazy
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.