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 assumes the key is always a string #51572

Closed
3dos opened this issue Nov 17, 2022 · 11 comments
Closed

Object.entries assumes the key is always a string #51572

3dos opened this issue Nov 17, 2022 · 11 comments
Labels
Duplicate An existing issue was already created

Comments

@3dos
Copy link

3dos commented Nov 17, 2022

lib Update Request

Configuration Check

My compilation target is ESNEXT and my lib is the default.

Missing / Incorrect Definition

Object.entries()

Sample Code

const obj: Record<'a' | 'b', string> = { a: 1, b: 2 };
const entries = Object.entries(obj); // is typed as [string, string][] instead of ['a' | 'b', string][]

Fix

Possible fix would be to update the definition in lib.es2017.object.d.ts like this:

    /**
     * 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<K, T>(o: Record<K, T> | ArrayLike<T>): [K, T][];
@MartinJohns
Copy link
Contributor

This is intentional. Objects are not sealed. See #38520 for more information.

@ahejlsberg ahejlsberg added the Duplicate An existing issue was already created label Nov 17, 2022
@3dos
Copy link
Author

3dos commented Nov 17, 2022

Well, I think the use case I stated here is a bit different than being based dynamically on keys. Instead of using string as the key type, there's a union type restricting the key type to a stricter one.

The fix I suggested is probably bad though :D but I don't see how this relates to the {} object becoming a never[] example.

Again, I probably miss something here and probably lack of understanding the underlying issue.

@MartinJohns
Copy link
Contributor

You're assuming the keys can only be "a" | "b" because the object is typed Record<'a' | 'b', string>, but that's wrong. All it says is that the keys "a" and "b" are typed string, but not that there aren't any other keys.

@3dos
Copy link
Author

3dos commented Nov 17, 2022

This is where I fail to understand what additional keys could possibly be there. As per the MDN docs for Object.entries() it seems only the actual keys are created. Is there a real life example of such divergence in the object/entries keys that could help me get this?

Anyway, is there a way aside from casting with as [TheUnionTypeOfMyKeys, TheValueType][] to get the stricter type as a result?

I'll read the whole thread again later on to make sure I didn't miss any crucial piece of explanation. Thank you for trying to explain this to me.

@alexburner
Copy link

Is there a real life example of such divergence in the object/entries keys that could help me get this?

Here's a good example: #12253 (comment)

TypeScript uses structural typing, not exact types. So extra keys may be allowed on an object, outside its type.

@fatcerberus
Copy link

On the subject of exact types: #12936

@nandorojo
Copy link

nandorojo commented Nov 17, 2022

For what it's worth, I use these custom wrappers:

export const entries = Object.entries as <T>(
  obj: T
) => Array<[keyof T, T[keyof T]]>
export const keys = Object.keys as <T>(obj: T) => Array<keyof T>
export const values = Object.values as <T>(obj: T) => Array<T[keyof T]>

Here's a playground.

Screen Shot 2022-11-17 at 3 17 52 PM

@MartinJohns
Copy link
Contributor

@nandorojo These wrappers are fairly common. Personally, I'd add the prefix unsafe, to make really clear that these wrappers are unsafe.

@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.

@dev-kg
Copy link

dev-kg commented Mar 15, 2023

just do this and the problem is gone

declare global {
  interface ObjectConstructor {
    keys<T>(o: T): (keyof T)[]
    // @ts-ignore
    entries<U, T>(o: { [key in T]: U } | ArrayLike<U>): [T, U][]
  }
}

I added // @ts-ignore because ts would tell me this:

Type 'T' is not assignable to type 'string | number | symbol

If someone have a solution to get rid of // @ts-ignore without loosing the ability to preserve the dynamic aspect of T, let us know in the comments

If this breaks your code you can do:

Object.tsKeys = function getObjectKeys<Obj>(obj: Obj): (keyof Obj)[] {
 return Object.keys(obj!) as (keyof Obj)[]
}
// @ts-ignore
Object.tsEntries = function getObjectEntries<U, T>(obj: { [key in T]: U }): [T, U][] {
 return Object.entries(obj!) as unknown as [T, U][]
}
declare global {
 interface ObjectConstructor {
   // @ts-ignore
   tsEntries<U, T>(o: { [key in T]: U }): [T, U][]
   tsKeys<T>(o: T): (keyof T)[]
 }
}

@SanariSan
Copy link

SanariSan commented Oct 30, 2023

I added // @ts-ignore because ts would tell me this:

I don't have such issue, but you may try to use PropertyKey keyword, which is what used for indexing objects under the hood (like T extends PropertyKey)

Also your first example produces union of values from every key which gives poor type narrowing even when explicitly checking the property:

image

You should follow the pattern above and use indexed access type instead of manually assigning union

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

9 participants