Typing Object Prototype Methods in Typescript

Mon Jul 11 2022

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.


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?


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.



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][]