Keyof inferring string | number when key is only a string

TypescriptTypescript Typings

Typescript Problem Overview


I define an AbstractModel like so:

export interface AbstractModel {
   [key: string]: any
}

Then I declare the type Keys:

export type Keys = keyof AbstractModel;

I would expect that anything with the Keys type would be interpreted univocally as a string, for example:

const test: Keys;
test.toLowercase(); // Error: Property 'toLowerCase' does not exist on type 'string | number'. Property 'toLowerCase' does not exist on type 'number'.

Is this a bug of Typescript (2.9.2), or am I missing something?

Typescript Solutions


Solution 1 - Typescript

As defined in the Release Notes of TypeScript 2.9, if you keyof an interface with a string index signature it returns a union of string and number

> Given an object type X, keyof X is resolved as follows:

> If X contains a string index signature, keyof X is a union of string, number, and the literal types representing symbol-like properties, otherwise

> If X contains a numeric index signature, keyof X is a union of number and the literal types representing string-like and symbol-like properties, otherwise

> keyof X is a union of the literal types representing string-like, number-like, and symbol-like properties.

source

This is because: JavaScript converts numbers to strings when indexing an object:

> [..] when indexing with a number, JavaScript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same thing as indexing with "100" (a string), so the two need to be consistent.

source

Example:

let abc: AbstractModel = {
    1: "one",
};

console.log(abc[1] === abc["1"]); // true

When you only want the string keys, then you could only extract the string keys from your interface like so:

type StringKeys = Extract<keyof AbstractModel, string>;

const test: StringKeys;
test.toLowerCase(); // no error

Also the TypeScript compiler provides an option to get the pre 2.9 behavior of keyof:

> keyofStringsOnly (boolean) default false

> Resolve keyof to string valued property names only (no numbers or symbols).

source

Solution 2 - Typescript

I have faced similar problem. I resolved it by enforcing key to be string:

export type Keys = keyof AbstractModel & string;

Other option would be to convert key to string: test.toString().toLowercase()

Solution 3 - Typescript

For a generic typescript utility, you can use the following:

type KeyOf<T extends object> = Extract<keyof T, string>;

Usage:

const sym = Symbol();

const obj = {
  [sym]: true,
  foo: 'foobar',
  bar: 'barfoo',
  1: 'lorem'
}

let key: KeyOf<typeof obj> = 'foo'; // 'foo' | 'bar'

key = 'bar'; // ok
key = 'fool'; // error
key = 1; // error

playground

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiondonView Question on Stackoverflow
Solution 1 - TypescriptjmattheisView Answer on Stackoverflow
Solution 2 - Typescriptmadox2View Answer on Stackoverflow
Solution 3 - TypescriptPoul KruijtView Answer on Stackoverflow