Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mapped types allow numeric constraint types #18346

Closed
wants to merge 2 commits into from

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Sep 8, 2017

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. 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:

// @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:

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:

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:

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 }.

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 }`.
@weswigham
Copy link
Member

weswigham commented Sep 9, 2017

@sandersn

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

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.

@sandersn
Copy link
Member Author

sandersn commented Oct 4, 2017

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.

@sandersn sandersn closed this Oct 4, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
@jakebailey jakebailey deleted the mapped-types-allow-numeric-constraint-types branch November 7, 2022 17:31
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants