Mapped types allow numeric constraint types#18346
Conversation
Mapped types now allow the constraint type to be `number`, a union of numeric literal types or an enum. When it is possible to treat an enum as a union of numeric literal types, the compiler does so. Numeric literal types create a property with the string form of the name. `number` and non-union enums create a number index signature on the mapped type. This enables most numeric enums to be used as the constraint type in mapped types: ```ts // @strict: true enum Nums { A, B } type NumBool = { [K in Nums]: boolean } let nb: NumBool = { '0': true, [1]: false } nb[Nums.A] = false ``` Note that any expression with the right literal type can be used to index into the resulting mapped type. This means that other enum entries are fine as long as they are in range of the original enum: ```ts enum Nums2 { Aleph, Bet, Gimel } nb[Nums.Aleph] = true // fine, equivalent to Nums.A and 0 and '0' nb[Nums.Gimel] = false // error only with 'strict': true ``` You can also manually create unions that mix string and number literal types: ```ts type Mixed = 0 | 1 | 'a' | '1' type MixNum = { [K in Mixed]: number } ``` If you do this be aware that if a number and a string literal conflict after the number has been converted to string, then both will be dropped from the mapped type: ```ts let mn: MixNum = { [0]: 8, [1]: 8, 'a': 8 } ~~~~~~ Object literal may only specify known properties, and '[1]' does not exist in type 'MixNum' ``` Non-union enums and `number` are just equivalent to a number index signature; `{ [K in number]: any }` is equivalent to `{ [n: number]: any }`.
Why this over a merging into the string type (actually, I think it must be merged, or some higher order relationships go weird)? Take this example: type TemplateOne = { x: "x" };
// This is done in two steps to cause the intersection to disappear in the final eager type :3
type AppendTemplateInner<T extends TemplateOne, Keys extends string | number> = T & {[K in Keys]: K};
type AppendTemplate<T extends TemplateOne, Keys extends string | number> = {[K in keyof AppendTemplateInner<T, Keys>]: AppendTemplateInner<T, Keys>[K]};
type TemplateTwo = AppendTemplate<TemplateOne, "y">;
type TemplateThree = AppendTemplate<TemplateTwo, "z">;
type TemplateFour = AppendTemplate<TemplateThree, "0">;
type TemplateFive = AppendTemplate<TemplateFour, 0>;
type AlsoTemplateFiveButNot = AppendTemplate<TemplateThree, 0 | "0">;Building the type one type at a time builds a different type than the union; which is super, super odd. I'm not sure, but it feels like that violates some expectations of the higher order relationships on mapped types. |
|
Here's a quick summary of the design meeting on this PR: a lot of mapped type behaviour relies on string keys and allowing numbers will make the language act badly in a number of places. We need a different design than the one implemented here. |
Fixes #13042
Mapped types now allow the constraint type to be
number, a union of numeric literal types or an enum. When it is possible to treat an enum as a union of numeric literal types, the compiler does so.Numeric literal types create a property with the string form of the name.
numberand non-union enums create a number index signature on the mapped type. This enables most numeric enums to be used as the constraint type in mapped types:Note that any expression with the right literal type can be used to index into the resulting mapped type. This means that other enum entries are fine as long as they are in range of the original enum:
You can also manually create unions that mix string and number literal types:
If you do this be aware that if a number and a string literal conflict after the number has been converted to string, then both will be dropped from the mapped type:
Non-union enums and
numberare just equivalent to a number index signature;{ [K in number]: any }is equivalent to{ [n: number]: any }.