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:
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)
@RyanCavanaugh Why is it legal to index it with both
number
andstring
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}
, butkeyof MappedFoo
isnever
!Playground link
type WithIndexKey = keyof WithIndex
is correctlynumber | string
– when a type has a string index signature, it’s legal to index it with bothnumber
andstring
values, sokeyof
represents that.WithoutIndexKey
beingnumber
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.