Typing Object Prototype Methods in Typescript
Properly typing anything in Typescript can be a huge pain, especially when it pertains to Javascript builtins. I ran into an issue today where I discovered Object.keys and Object.entries weren't returning the correct types I wanted.
Problem ¶
Pretend we have an object like the one below:
const obj: Record<number, string> = {
0: "Hello",
1: "World!",
}
Object.keys returns an array of all keys in a given object. In the example above, Object.keys(obj) returns [0, 1]. It only makes sense that the type of this result should be of type number[], right?
const x = Object.keys(obj) // const x: string[]
Why are the keys conforming to a string type? We would expect Object.keys to return keyof T, but it instead returns string. Is this a bug with Typescript?
Rationale ¶
Believe it or not, this is actually intended behavior in Typescript. The issue with assuming keyof T as the return type of Object.keys is that keyof T does not necessarily represent an exhaustive list of keys. Take the following valid Typescript example:
interface Database {
x: string
y: string
}
function run(k: keyof Database) {
if (k === "x") {
// do something with x
}
else if (k === "y") {
// do something with y
}
else {
// no other key should exist
}
}
If we extend this interface, we run into issues because keyof is not exhaustive.
interface Box extends Database {
z: string
}
const data: Box = {
x: "Hello",
y: "World!",
z: "Illegal!",
}
run(data) // uh-oh! z breaks everything!
Because keyof T does not and cannot possibly represent an exhaustive list of keys, Typescript instead declares Object.keys and other similar Object prototype methods to return keys of type string.
https://github.com/microsoft/TypeScript/issues/35101
Solution ¶
Since the type of Object prototype methods is semantically correct, is there a workaround for better typing on objects?
I'm glad you asked!
We can write utility functions to substitute Object prototype method types using type casts. Typescript natively provides a PropertyKey type representing any object key type (it literally translates to string | number | symbol
). We can utilize PropertyKey to write type casts for any builtin methods that coerce key types to strings.
const getObjectKeys = Object.keys as <T extends Record<PropertyKey, any>>(obj: T) => (keyof T)[]
const getObjectEntries = Object.entries as <T extends Record<PropertyKey, any>>(obj: T) => [keyof T, T[keyof T]][]
Now we have "better" typing:
const obj: Record<number, string> = {
0: "Hello",
1: "World!",
}
const x = Object.keys(obj) // const x: string[]
const y = getObjectKeys(obj) // const y: number[]
const a = Object.entries(obj) // const a: [string, string][]
const b = getObjectEntries(obj) // const b: [number, string][]