0

I'm trying to iterate through the key/value pairs in an object and in some cases want to overwrite some of them. To do that, I need to index into the object using the key, which is a string.

This works:

obj['a'] = 'new value';

This doesn't work:

obj[key] = 'new value';` //despite key being a string of value 'a'

The error 'string can't be used to index' can't be correct when it was happy to accept a hard-coded 'a' as an index.

Here's a screenshot from a simplified version of my code from the TypeScript playground. I'm working around this using @ts-ignore currently but I really wish I didn't have to.

enter image description here

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Dee2000
  • 1,641
  • 2
  • 18
  • 21
  • `string` is broader than `'a'`, there are lots of strings that _aren't_ valid keys. But please put a [mre] in the question, not a screenshot (you can accompany it with a _link_ to the playground if you wish). Canonical: https://stackoverflow.com/q/55012174/3001761. – jonrsharpe Apr 21 '23 at 19:00
  • What are the `target` value and the others in your `tsconfig.json`? Your code doesn't seem to be a problem for ES2017 for example. – Igor Micev Apr 21 '23 at 19:34

4 Answers4

1

Your problem is that obj's type is { a: string, b: string }, and as such, the only valid keys are "a" and "b"; while Object.keys returns strings. If it returned the type Array<keyof obj> you wouldn't have this problem; but it doesn't because of inheritance - the reasoning is explained in this question.

To fix this, since you know this object doesn't have more keys than its type indicates (because its defined right above the loop), you can use as to assert that the value is of the correct type:

let obj = { a : 'A', b: 'B'}

for (const key in obj) {
    console.log(obj[key as keyof typeof obj])
}

However, if the map isn't being created inside the same function that is iterating over it, you could specify its type as {[key: string]: string}:

let obj: {[key: string]: string} = { a : 'A', b: 'B'};

for (const key in obj) {
    console.log(obj[key])
}
Leonardo Dagnino
  • 2,914
  • 7
  • 28
  • This is perfect for me. I thought keyof might be part of the solution, but being relatively new to TypeScript I wasn't getting the syntax right. I had tried [key as keyof obj] but there's no way on earth I was ever going to think of trying [key as keyof typeof obj] :-) – Dee2000 Apr 22 '23 at 10:05
0

Something like this worked for me a few days ago. I had this exact problem.

  function narrowTheType(startString: string): 'a' | 'b' | 'c' | 'd' {
    if (startString === 'a') return 'a'
    if (startString === 'b') return 'b'
    if (startString === 'c') return 'c'
    return 'd'
  }

By putting the key value into the function, you get the output string value of a specific type that TypeScript is aware of. Downside: you have to type out all of the specific strings key could be.

plutownium
  • 1,220
  • 2
  • 9
  • 23
  • Thanks, but that's not something for me, the input object in my real function has to accept an object with potentially a hundred different values. It can have any random selection of those properties in it. – Dee2000 Apr 22 '23 at 10:10
0

Not really sure what your use case is but as I understand it, this is due to Typescript not being able to determine the type of obj as it has an implicit any. The following should resolve the issue.

let obj:any = { a : 'A', b: 'B'};

for (const [key] of Object.entries(obj)) {
    console.log(obj);
    obj[key] = "C";
    console.log(obj);
}
  • I don't get why defining it as any improves things over an implicit any. I'd expect that to bring no gains. I've accepted an answer here now though. – Dee2000 Apr 22 '23 at 10:12
0

Overwrite the return type of Object.entries should help, but note that it may break any place where use Object.entries

declare global {
  type EntriesPair<T> = T extends T
    ? {
        [K in keyof T]: K extends string | number ? [K extends string ? K : `${K}`, T[K]] : never
      }[keyof T]
    : never
  type Entries<T> = NonNullable<EntriesPair<T>>[]
  interface ObjectConstructor {
    entries<T extends {}>(o: T): Entries<T>
  }
}

export {}
Jerryh001
  • 766
  • 2
  • 11
  • Thanks but it's that type of syntax that blows my mind a bit. I really struggle to understand how to even read those, never mind write them. I can believe that totally works but if someone criticizes it and asks for a bit of a re-write in a code review I'd be lost as to how to modify it in any way. I'm sure I'll get comfortable with that stuff down the line but not yet. For others finding this in future, the other answer from Leonardo might not be enough for them but it is for my case. – Dee2000 Apr 22 '23 at 10:08