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 isn't subscriptable with string key when enumerating keys #56787

Closed
benasher44 opened this issue Dec 14, 2023 · 8 comments
Closed

object isn't subscriptable with string key when enumerating keys #56787

benasher44 opened this issue Dec 14, 2023 · 8 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@benasher44
Copy link

benasher44 commented Dec 14, 2023

πŸ”Ž Search Terms

  • 'string' can't be used to index type '{}'
  • subscript
  • key
  • enumerate

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about "object" and "keys"

⏯ Playground Link

https://www.typescriptlang.org/play?#code/MYewdgzgLgBAhjAvDA3jKAnArgUwFwwCMMAvgNwCwAUNQGZZjBQCW4MWADgCZxQ4CCAG0EAFDCA4QAPABUYOAB58wXCKhIA+ABQgARgCsCMgJSpqMGLRAYYW0JFgBrHAE8YIWjADyBnEwB0zi4QOgbGpijmFu4GANpBALpIMABMlFQWJDDUJNScPHxCouKSWnDG1NT2ECCCOP6CIADmZcZAA

πŸ’» Code

const a = { true: 1 };

function updateAllProps<T extends {}>(obj: T) {
  for (const key of Object.keys(obj)) {
    obj[key] = 2;
  } 
}
updateAllProps(a)

console.log(a)

πŸ™ Actual behavior

TS emits: "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
No index signature with a parameter of type 'string' was found on type '{}'."

πŸ™‚ Expected behavior

In the context of Object.keys and Object.entries, TS should allow indexing using the returned key. As much as I dislike the below code, it is valid:

const a = { true: 1 };
a["true"] = 2; // results in a now looking like { true: 2 }

Additional information about the issue

No response

@benasher44 benasher44 changed the title object isn't subscriptable with string key object isn't subscriptable with string key when enumerating keys Dec 14, 2023
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Dec 14, 2023
@RyanCavanaugh
Copy link
Member

Object.keys just returns string[] (this is intentional). If you want to write to an object at an arbitrary string, use a type that allows that.

@benasher44
Copy link
Author

benasher44 commented Dec 14, 2023

In this particular case, I'm writing a function to search arbitrary objects. I want to enumerate all properties and update them using Object.keys/Object.entries, and I want TypeScript to be okay with that.

The production code looks more like:

if (typeof thing === "object" && thing !== null && !Array.isArray(thing)) {
  // at this point, all we know is that it's an object
  // enumerate object props
}

Ideally, TypeScript would be okay with this indexing, since JS allows it without issue and behaves as expected.

@jcalz
Copy link
Contributor

jcalz commented Dec 15, 2023

See #13254 and #12253 (comment)

@MartinJohns
Copy link
Contributor

TypeScript would be okay with this indexing, since JS allows it without issue and behaves as expected.

This is such a weird argument. I use TypeScript because it doesn't allow all the crap that JavaScript allows.

@nmain
Copy link

nmain commented Dec 15, 2023

If your coding style and guidelines give you more predictability on what properties arbitrary objects have (or maybe what properties specific subsets of objects have), you can add more overloads to Object.keys, Object.values, and/or Object.entries, or you can write your own helper methods that use as assertions internally while presenting strong types to consumers.

@benasher44
Copy link
Author

benasher44 commented Dec 15, 2023

@MartinJohns @nmain I promise I appreciate strong mapped types as much as anyone else.

The issue is that Object.keys and Object.entries always returns string keys, regardless of the type. Object.keys({ 1: 2 }) returns [ '1' ], for example. I would of course much prefer that there were a type-safe way to enumerate arbitrary keys and have type-safe setters for them, but there isn't (in all cases). There is no casting to be done, since the types for Object.keys are correct. At least for the node runtime, (looking at docs again, runtime doesn't matter) This is what you get when using these functions.

In this particular case, I'm writing a function that enumerates an arbitrary object graph that comes from another library, which is attached to error metadata, so it's not an issue of "fix your code base to be better" β€”Β not my code base :). If it helps, I will offer that I really did not enjoy writing this code.

If you find yourself in the position of writing code like this, my issue is that you will have to cast the object to Record<string, any> or Record<string, unknown> in order to subscript with the keys from these Object functions because TypeScript will claim that the object isn't subscriptable with string, even though it absolutely is. Is it "crap" that JS allows it? I agree with you, yes.

My ask is that in the particular case where the keys come from calling Object.keys or Object.entries on an object, the keys be allowed to subscript that object without casting because, well, they are. While this case is somewhat narrow, writing utility-type code that enumerates keys and values to manipulate the object based on some rule/predicate isn't uncommon.

I hope that clarifies the ask. I can understand if/why TS won't take up an improvement here. There is a workaround, which is you do the classic:

for (const key in obj) {
  if (Object.hasOwn(obj, key)) {
    obj[key] = …
  }
}

TS has no issue with this, but I think the ergonomics of Object.keys and Object.values are better.

@fatcerberus
Copy link

fatcerberus commented Dec 15, 2023

Object.keys({ 1: 2 }) returns [ '1' ]

Not always.

const foobar = { foo: "foo", bar: 777 };
const foo: { foo: string } = foobar;
console.log(Object.keys(foo));  // [ "foo", "bar" ]

...which is why Object.keys() returns string[]; objects can have extra properties that the type system doesn't know about. Returning Array<"foo"> here would be unsound because the array will also contain "bar". In fact this is exactly what TS is protecting you from, because if you're getting these objects from some library, they could very well have extra properties for internal use (that aren't reflected in the type definitions) that you really don't want to be overwriting.

My ask is that in the particular case where the keys come from calling Object.keys or Object.entries on an object, the keys be allowed to subscript that object without casting because, well, they are.

This would require keyof types to be opaque, which is also problematic, because then... well, what is the type of obj[key]? πŸ˜‰ Also TS doesn't do the kind of tracking where it can know a complex operation should be allowed simply because it reuses a variable - it only sees types. Hence why we have issues like #30581.

@benasher44
Copy link
Author

Ah makes sense β€” thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants