Typescript - What is the better way to cast the return value of Object.keys()?

Viewed 2027

I think the code will speak by itself, so here it is:

type myType = "a" | "b" | "c";
type myMappedType = { [str in myType]: number };

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

const keys = Object.keys as <T>(o: T) => Extract<keyof T, string>[];
const keys2 = Object.keys as <T>(o: T) => Array<keyof T>;

Object.keys(values) // string[]
keys(values) // myType[]
keys2(values) // myType[]

What is the better way to cast the return value of Object.keys()?

  • Should I use the keys or the keys2 implementation? Or even another one?
  • Which one is better and why?

Thanks in advance!

2 Answers

I think the keys version is more correct. The main difference is keys2 will include number and Symbol keys in the resulting type, but the symbol keys will not be in the result of Object.keys.

I'd include number in the extract type though:

const keys = Object.keys as <T>(o: T) => Extract<keyof T, string | number>[];

Object.keys will return numeric keys (as strings), but they will be missing from the return type if you don't make this change.

Even using Record<myType, number>, Object.keys will return a string[]. If you were to use an ES6 Map, however,

type myType = "a" | "b" | "c";

const mapValues = new Map<myType, number>(
    [
     ['a', 1]
     , ['b', 2]
     , ['c', 3]
    ]
)

mapValues.keys() // IterableIterator<myType>

Map contructors are a tad bit less ergonomic than object literals, but they seem to work better with TypeScript's typing. I tried in vain to dig up an old thread about object typing that lays out the weird edge cases with it.

Related