Even though object keys are always strings under the hood, and typing indexers as strings covers numbers, sometimes you want a function to be aware of the keys of objects being passed into it. Consider this mapping function which works like Array.map but with objects:
function map<T>(obj: Object, callback: (key: string, value: any) => T): T[] {
// ...
}
key is restricted to being a string, and value is entirely untyped. Probably fine 9 out of 10 times, but we can do better. Let's say we wanted to do something silly like this:
const obj: {[key: number]: string} = { 1: "hello", 2: "world", 3: "foo", 4: "bar" };
map(obj, (key, value) => `${key / 2} ${value}`);
// error: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.
We can't perform any arithmetic operations on key without first casting it to a number (remember: "3" / 2 is valid in JS and resolves to a number). We can get around this with a little bit of tricky typing on our map function:
function map<S, T>(obj: S, callback: (key: keyof S, value: S[keyof S]) => T): T[] {
return Object.keys(obj).map(key => callback(key as any, (obj as any)[key]));
}
Here, we use the generic S to type our object, and look up key and value types directly from that. If your object is typed using generic indexers and values, keyof S and S[keyof S] will resolve to constant types. If you pass in an object with explicate properties, keyof S will be restricted to the property names and S[keyof S] will be restricted to the property value types.