I am struggling to understand TypeScript's types when using the keyof type operator on objects.
Have a look at the following example:
type TypeA = { [k: number]: boolean };
type AKey = keyof TypeA;
// ^? type AKey = number
type TypeB = { [k: string]: boolean };
type BKey = keyof TypeB;
// ^? type BKey = string | number
The TypeScript documentation contains a note saying something like:
Note that in this example,
keyof { [k: string]: boolean }isstring | number— this is because JavaScript object keys are always coerced to a string, so obj[0] is always the same as obj["0"].
That make it seems as if it should be the exact opposite.
That AKey should be number (because they are always coerced to a string), and BKey should just be a string, because the type doesn't allow numbers.
And if that isn't confusing enough, the same doesn't hold true when using Record<>.
That seems to be because the definition uses in instead of ::
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
type TypeC = { [k in number]: boolean }; // Record<number, boolean>
type CKey = keyof TypeC;
// ^? type CKey = number
type TypeD = { [k in string]: boolean }; // Record<string, boolean>
type DKey = keyof TypeD;
// ^? type DKey = string
All types do allow using both numbers and strings as keys, so the type definitions don't seem to affect that in any way:
const value: TypeA | TypeB | TypeC | TypeD = {
0: false,
"1": true,
};
Can anyone help me understand this type circus?
The behavior of the
keyofoperator when applied to mapped types and to types with index signatures is specified in the implementing pull request at microsoft/TypeScript#23592. The rules are:TypeScript treats numeric keys of type
numbera subtype of keys of typestring(this is a convenient fiction to support array indexing; object keys are never reallynumber, but instead numeric strings. But I digress, see keyof type operator with indexed signature for more). That means every numeric key is really a string key, but not every string key is a numeric key. An object with a numeric index signature does not claim to support every string key, while an object with a string index signature does claim to support every string key, which includes numeric keys as well.As for the difference in behavior of
keyof Record<string, T>isstringwhilekeyof {[k: string]: T}isstring | number, the mapped type behavior was unchanged by the pull request, so it's possible to confirm that it is this way. As for why it's this way, that's harder to say for certain. The closest I can find to a canonical answer for that is a comment in microsoft/TypeScript#31013 by the TS dev team lead:So, presumably
keyof {[P in K]: T}beingKis important to preserve even ifKisstring, and the reason it is inconsistent withkeyof {[k: string]: T}is because the differing syntax allows to choose between the two behaviors. I guess. Anyway it points back to microsoft/TypeScript#23592.