TypeScript: Using keyof with key remapping to exclude index signatures doesn't return known keys

TypeScript Version: 4.2.0-dev.20201211

Search Terms:

key remapping remap index signature keyof returns number

Code

type Compute<A> = { [K in keyof A]: Compute<A[K]>; } & {};

type EqualsTest<T> = <A>() => A extends T ? 1 : 0;
type Equals<A1, A2> = EqualsTest<A2> extends EqualsTest<A1> ? 1 : 0;

type Filter<K, I> = Equals<K, I> extends 1 ? never : K;

type OmitIndex<T, I extends string | number> = {
  [K in keyof T as Filter<K, I>]: T[K];
};

type IndexObject = { [key: string]: unknown; };
type FooBar = { foo: "hello"; bar: "world"; };

type WithIndex = Compute<FooBar & IndexObject>;   // { [x: string]: {}; foo: "hello"; bar: "world"; } <-- OK
type WithoutIndex = OmitIndex<WithIndex, string>; // { foo: "hello"; bar: "world"; }                  <-- OK

type FooBarKey = keyof FooBar;             // "foo" | "bar"   <-- OK
type WithIndexKey = keyof WithIndex;       // string | number <-- Expected: string 
type WithoutIndexKey = keyof WithoutIndex; // number          <-- Expected: "foo" | "bar"

Expected behavior:

Using keyof with the above remapped type (WithoutIndex) should return the known keys (i.e. "foo" | "bar").

Actual behavior:

While the key remapping in OmitIndex appears to successfully omit the string index signature (e.g. OmitIndex<WithIndex, string> --> { foo: "hello"; bar: "world"; }); using keyof with the resulting type returns number.

Playground Link:

https://www.typescriptlang.org/play?ts=4.2.0-dev.20201211#code/C4TwDgpgBAwg9gWzAV2BAPAQQHxQLxQDeUA2gNJQCWAdlANYQhwBmUmAugFyyIppbl22ANxQAvlABkRMcIBQc0JCgBRAI7IAhgBsAzgBUIu4On24CWbAAoAlPlyYoEAB5pqAE11R9UAPxQARihuAAZ5JWh1LT0sAIAaNgAmc1UNHQMjE0xkp1cIDy8o9MNjWNx-IND5RXBoADFKbTQAJ3QyBIBJFKKY9qgu3LdPQL8oaggANwhm4KgyaoioAHkESmAOjxdTTsH84eNmmgBzKAAfMeQEACNplMI5KFIKGnpGFm8oTS8Gpum2zuwXG8gnksgUiw27hcSyuACsIABjYD4IikBggbgHY5A5DUOjUOAAd2oojBizqcDgACFNDMCMRmJTuAAiAAWEG02jgzNEV1pLMJcGa2ncPPEC1qUAA6mtWZCXCj4EhUBgKdTaVJ+ptnDD4UiRI8oAB6I2okjOTHAQ7UI5AwiyKCMuAs9mc7m8-lQZmC4Wi0lQdAAWkDyzINWUMuArLgqHlzhRKzWcfQkbl2oSWJtBpNqKdLo5XLFfOaAqFIrFEkNVer1aDIaWYfD9UpNOaZEYKPR7zVrdENZrOeZTuZZy9xZHjzroab0tlcfbIE7b1YqbjferOczJ3O1EuNxmU5UzkgSIg7kt1pOM9TMfW2oXS6YK9lt7XxtNu+u037hsPx8RaDnl6w6jsy45yEAA

Related Issues:

https://github.com/microsoft/TypeScript/issues/31143 https://github.com/microsoft/TypeScript/issues/41383 https://github.com/microsoft/TypeScript/issues/38646

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 26 (23 by maintainers)

Most upvoted comments

@RyanCavanaugh Why is it legal to index it with both number and string values? In untyped JS it’s legal to add a number to a string, e.g. "n" + 0 + "nsense" will happily produce "n0nsense", but TypeScript sensibly prevents us from doing this. Seems to make sense that it should also prevent us from indexing a type that is explicitly declared as having string keys with a number. If I want { [key: string]: X }, I’ll declare that, and if I want { [key: string | number]: X } I’ll declare that.

The implicit coercion from numbers from strings is usually fine in situations that are “contravariant” in the key type (e.g. indexing), but it ends up producing all kinds of nonsense that needs to be casted/Extract-ed away when the keys are reified and you try to do something with them.

EDIT: Wow, apparently TypeScript doesn’t prevent "n" + 0 + "nsense".

I just ran into the following situation which was not resolved by #41713. Below, MappedFoo is {a: string, b: string}, but keyof MappedFoo is never !

type MapKnownKeys<T> = {
   [K in keyof T as string extends K ? never : number extends K ? never : K]: string;
}

interface Foo {
   a: number,
   b: number,
   [k: string]: unknown;
}

type MappedFoo = MapKnownKeys<Foo>;
/* type MappedFoo = {
    a: string;
    b: string;
} */
const mappedFoo: MappedFoo = {
   a: "",
   b: "",
}; // okay

type KeysOfMappedFoo = keyof MappedFoo;
// type KeysOfMappedFoo = never 💥
const a: KeysOfMappedFoo = "a"; // error!
//    ~ <-- Type 'string' is not assignable to type 'never'

Playground link

type WithIndexKey = keyof WithIndex is correctly number | string – when a type has a string index signature, it’s legal to index it with both number and string values, so keyof represents that.

WithoutIndexKey being number is super wrong, though 😲

Any behavior is going to be “inconsistent” if you squint at it from the right angle since numeric index signatures are purely a fiction used to denote a rough class of objects with common behavior, rather than a real runtime distinction.