-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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.keys() types refinement, and Object.entries() types bugfix #12253
Conversation
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.
Applying the same logic used on Object.entries in the prior commit and in microsoft#12207
Hi @ethanresnick, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution! The agreement was validated by Microsoft and real humans are currently evaluating your PR. TTYL, MSBOT; |
@@ -210,6 +210,8 @@ interface ObjectConstructor { | |||
* Returns the names of the enumerable properties and methods 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. | |||
*/ | |||
keys<T>(o: Array<T>): string[]; | |||
keys<T extends { [key: string]: any }>(o: T): (keyof T & string)[]; |
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)[];
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.
@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
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.
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.
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.
Now that keyof T only returns string or string subtypes (microsoft#12425), there’s no need to do keyof T & string.
@mhegazy I've updated this PR to replace Cc @ahejlsberg: you expressed skepticism about typing Object.keys with keyof in #12289 (comment). But if/now that |
@ethanresnick My reservations about BTW, I have the same reservations about Note that the changes we made to |
@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. |
@ahejlsberg And what about using it on |
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... |
@dyst5422 Keys are returner as strings. var a={1:2}; |
@ahejlsberg I'm not sure I understand your reasoning. If you're object has type |
@ccorcos no, because lots of objects with other extraneous keys can have type 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?
}
|
I see. Sounds like that should be a type error then. haha |
I understand the reasoning here and it makes sense, but interface Foo { foo: string; }
declare const foo: Foo
Object.keys(foo).forEach((key: keyof Foo) => { }) // Error
Which is a pain because with 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 That's pretty annoying. Is there no way we could allow |
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? 🤔 |
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)[];
} |
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. 🙂 |
I believe the updated version for 2.9 is export const keys = Object.keys as <T>(o: T) => (Extract<keyof T, string>)[]; |
Per @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(That part of the PR is now covered by #12425.)string | number
.