What's the cleanest way to tell typescript that each element of Object.keys(foo) is really a key of foo?

Viewed 72

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.

4 Answers

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.

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

const foo: { [key: string]: number } = {
    a: 1,
    b: 2,
    c: 3
};

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

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]++;
});
Related