4

playground

const foo = {
  a: 1,
  b: 2,
  c: 3
};

Object.keys(foo).forEach(key => {
  foo[key]++; // error: expression of type 'string' can't be used to index ....
})

There must be multiple ways to solve it. I'm looking for a recommended way with least impact on readability. Thanks!

==========

Edit:

Bonus: would be great if I can still have autocompletion when I type foo. in my codebase.

Quuxuu
  • 549
  • 1
  • 4
  • 10
  • 1
    `(key: keyof typeof foo)`? See e.g. https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript for discussion. – jonrsharpe Sep 08 '20 at 07:32
  • [This](https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZiEMC8MDeAoGMCGAuGARgBosYAjAgJlO2AIGYMBfAbgw1ElgGsBTAJ4QUMAPLkAVn2BQAdPyG5hAHgAqAPgAUIAqoCUKdTE0BRAB5QATjhnKFIODFXEY0SwEswAc3V6A2gC67Jzg0PCIANKCwqgKEJoIIHrBXBAgADZ8sukgXgmR0ckwAPTFMH4ARDgVLhXkNTAVwBUBQA) is the solution from the github issue linked in that related question. It uses `Extract` to get all keys that are assignable to `string` –  Sep 08 '20 at 07:59

4 Answers4

2

This should work:

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

This isn't automatically done for you, because in theory it could be unsafe, but I played with various scenarios and couldn't produce a working demo with a runtime error.

ZYinMD
  • 3,602
  • 1
  • 23
  • 32
  • You can find such a scenario if you follow the link from jonrsharpe's comment on the question. Briefly, the problem is that someone could do: `let o: any = foo; o.x = 'hello'`. That is, objects are allowed to have surplus properties in typescript, which is why Object.keys might return a key not present in the type of foo. – meriton Sep 08 '20 at 13:38
0

I think the cleanest way would be to add a type to foo.

const foo: { [key: string]: number } = {
    a: 1,
    b: 2,
    c: 3
};
MoxxiManagarm
  • 8,735
  • 3
  • 14
  • 43
  • This allows for example `foo.d = 1`. Foo's exact type would be `{ a: number; b: number; c: number }` –  Sep 08 '20 at 08:03
0

Type foo as a Record:

const foo: Record<string, number> = {
    a: 1,
    b: 2,
    c: 3
};

Object.keys(foo).forEach(key => {
    foo[key]++;
})

See the updated playground

thomaux
  • 19,133
  • 10
  • 76
  • 103
  • 1
    This also fails due to `foo.d = 1` being possible. The exact type of foo should be `{ a: number; b: number; c: number }` –  Sep 08 '20 at 08:10
  • @MikeS. true, depends on what the OP actually needs, the way the question was asked made me believe they were handling a dictionary like object – thomaux Sep 08 '20 at 08:36
0

TypeScript provides no way to express this at the time foo is declared because somebody might later add additional properties to foo, for instance by:

let o: any;
o = foo;
o.x = 'hello';

Then, Object.keys(foo) will also return x, but trying to increment 'hello' is probably not what you intend ...

More generally, in typescript, an object may always contain surplus properties, because this is what allows you to do:

interface Person = {
    name: string;
}

interface Student extends Person {
    university: string;
}

let student: Student = ...;
let person: Person = student;

That is, TypeScript can never be sure that an Object has no surplus properties, and that's why Object.keys returns Array<string> rather than Array<keyof typeof foo>.

In your case, I might do:

const fooKeys = Object.keys(foo) as Array<keyof typeof foo>;

right after declaring foo. That way, we take a snapshot of the keys we intend to iterate over before anybody else has a chance to modify them. Then, you can do:

fooKeys.forEach(key => {
    foo[key]++;
});
meriton
  • 68,356
  • 14
  • 108
  • 175
  • Accepted this answer, but note that [ZYinMD's answer](https://stackoverflow.com/a/63794568/8238314) is good too (and cleaner) if you write the loop at the same time when the desired value of `foo` is produced / imported / declared. – Quuxuu Sep 09 '20 at 20:32