-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Document mapped object types in spec #20971
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -527,6 +527,154 @@ is indistinguishable from the type | |
{ first: string; second: Entity; } | ||
``` | ||
|
||
|
||
# Mapped Object Types { #mapped-object-types } | ||
|
||
  *MappedObjectType:* | ||
   `{` `readonly`*<sub>opt</sub>* `[`*Identifier* `in` *Type*`]` `?`*<sub>opt</sub>* *TypeAnnotation<sub>opt</sub>* `}` | ||
|
||
A ***mapped object type*** is a type operator that operates on types assignable to the string type, but primarily on unions of string literal types. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would change "but primarily" to "usually" |
||
|
||
In the above syntax, | ||
|
||
* The first *Type* (immediately following the `in` keyword) is the operand *K* of a mapped type. | ||
* The second *Type* forms the *property type template* of a mapped type, *T*. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is marked as a type annotation in the grammar. Instead of referencing the grammar, I would give an example of the syntax first: |
||
* The *Identifier* forms a type variable *P* that is scoped only in *T*, and is bound and constrained to *K*. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A syntactic form doesn’t form a semantic object. It, uh, creates it? Not sure actually. Results in? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not following on the question here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The verb used is wrong. Syntax doesn't form semantic things like type variable, so an identifier doesn't form a type variable. You might be able to say "The identifier specifies a type variable P" or "The identifier defines a type variable P" or "The identifier references a type variable P". The last two sound best to me, but I think defines is correct here. |
||
|
||
Mapped object types are primarily meant to iterate over a union of string literal types, and to generate a new object type containing properties whose names are based on each string literal within that union. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You already mentioned that mapped types usually operate on string literals. I would reword to "When the operand K is a union of string literal types, mapped object types iterate over ..." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "are based on" is vague. Maybe "properties which take their names from the value of the members of the string literal union", or a more efficient variant. |
||
For example, in the below example, the type aliases `Foo` and `Bar` are equivalent even though `Foo` aliases an object type literal, and `Bar` aliases a mapped object type. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would inline this and remove the aliases: "For example, |
||
|
||
```ts | ||
type Foo = { hello: string, beautiful: string, world: string; }; | ||
|
||
type Bar = { [P in "hello" | "beautiful" | "world"]: string }; | ||
``` | ||
|
||
The operand is not required to be a union of string literal types, but is only required to be a assignable to string. | ||
|
||
```ts | ||
type A1 = { [P in "hello"]: string }; | ||
type A2 = { hello: string }; | ||
|
||
type B1 = { [P in string]: number }; | ||
type B2 = { [P in any]: number }; | ||
type B3 = { [propName: string]: number }; | ||
``` | ||
|
||
In the above, `A1` is equivalent to `A2`, and both types contain only a single property named `hello` of type `string. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. Inline. |
||
`B1`, `B2`, and `B3` are also equivalent, and introduce object types with only a string index signature. | ||
|
||
Like with property names, mapped object type allow the `readonly` and the `?` optionality modifiers to be specified as well. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete 'with' |
||
Specifying a `readonly` modifier on a mapped type results in each property or index signature of the mapped object type becoming read-only. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. d Specifying |
||
Similarly, specifying a `?` results in each property becoming optional, or in the case where an index signature is generated, an index signature whose type forms a union with the Undefined type. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. d specifying |
||
In the below example, `A1` is equivalent to `A2`, `B1` is equivalent to `B2`, and `C1` is equivalent to `C2`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline or make a table. A1 and A2 are needless indirection here. Defining some special punctuation as an assignability (or identicality, or equivalence) operator would help inlining. Maybe ↠ ? |
||
|
||
```ts | ||
type A1 = { readonly [P in "hello" | "world" ]: string }; | ||
type A2 = { | ||
readonly hello: string, | ||
readonly world: string, | ||
} | ||
|
||
type B1 = { [P in "foo" | "bar"]?: number }; | ||
type B2 = { | ||
foo?: number, | ||
bar?: number, | ||
}; | ||
|
||
type C1 = { readonly [P in string]?: boolean }; | ||
type C2 = { | ||
readonly [propName: string]: boolean | undefined; | ||
}; | ||
``` | ||
|
||
As mentioned, mapped object types introduce a type variable *P*. | ||
When *K* is not a generic type, as seen thus far, then when generating each property of a mapped object type, the type of that property is *T* with instances of *P* substituted with a string literal type whose contents are equivalent to the property name itself. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a lot better to phrase this something like "in the scope of the template type, P is each string literal type in the union K for properties and P is K for index signatures. |
||
Similarly, when generating an index signature, the type of that index signature prior to accounting for optionality is *T* with instances of *P* substituted with *K*. | ||
In the following example, each pair `A1` and `A2`, `B1` and `B2`, `C1` and `C2`, `D1` and `D2`, are respectively equivalent. | ||
|
||
```ts | ||
type A1 = { [P in "hello" | "world"]: P }; | ||
type A2 = { | ||
hello: "hello", | ||
world: "world", | ||
}; | ||
|
||
type B1 = { [P in "hello" | "world"]: P | boolean }; | ||
type B2 = { | ||
hello: "hello" | boolean, | ||
world: "world" | boolean, | ||
}; | ||
|
||
type C1 = { [P in string]: P }; | ||
type C2 = { | ||
[propName: string]: string, | ||
}; | ||
|
||
type D1 = { [P in any]: P }; | ||
type D2 = { | ||
[propName: string]: any, | ||
}; | ||
``` | ||
|
||
This can be powerfully combined with key query types, and indexed access types: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. combined powerfully reads better to me, but the sentence is a little weird either way. |
||
|
||
```ts | ||
interface TypeMap { | ||
"str": string, | ||
"num": number, | ||
"bool": boolean, | ||
} | ||
|
||
interface SchemaType { | ||
foo: "str", | ||
bar: "num", | ||
baz: "bool", | ||
} | ||
|
||
type TypeScriptType = { | ||
[P in keyof SchemaType]: TypeMap[P] | ||
}; | ||
|
||
// Equivalent to... | ||
interface TypeScriptType { | ||
foo: string, | ||
bar: number, | ||
baz: boolean, | ||
} | ||
``` | ||
|
||
## Homomorphic Mapped Object Types { #homomorphic-mapped-object types } | ||
|
||
A ***homomorphic mapped object type*** is a mapped type of a particular form, where the operand *K* is a type query `keyof` *O*. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. d of a particular form, |
||
|
||
In such instances, TypeScript will consult the type `O` when generating each property and index signature for respective modifiers. | ||
If a modifier is not specified in the mapped type itself, but is specified for a given property in *O*, then the resulting property inherits that same modifier. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In such instances, TypeScript uses the properties and index signatures in O as the source of modifiers for the properties and index signature of the mapped type. |
||
For example, in the following, `A` and `B` are equivalent types, but `C` is not because it does not represent a homomorphic mapped type. | ||
As a result, the `baz` property is `readonly` in `A` and `B`, but not in `C`. | ||
|
||
```ts | ||
interface T { | ||
foo?: number | ||
bar: number; | ||
readonly baz?: string; | ||
} | ||
|
||
type A = { | ||
foo?: number; | ||
bar: number; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bar should be optional |
||
readonly baz?: string; | ||
} | ||
|
||
type B = { | ||
[P in keyof T]?: T[P]; | ||
} | ||
|
||
type C = { | ||
[P in "foo" | "bar" | "baz"]?: T[P]; | ||
} | ||
``` | ||
|
||
## Specifying Types { #specifying-types } | ||
|
||
Types are specified either by referencing their keyword or name, or by writing object type literals, array type literals, tuple type literals, function type literals, constructor type literals, or type queries. | ||
|
@@ -549,6 +697,7 @@ Types are specified either by referencing their keyword or name, or by writing o | |
   *PredefinedType* | ||
   *TypeReference* | ||
   *ObjectType* | ||
   *MappedObjectType* | ||
   *ArrayType* | ||
   *TupleType* | ||
   *TypeQuery* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is TypeAnnotation optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it's optional :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WOW. Why do we support this? It looks like if TypeAnnotation is missing, then it's treated as if it were
: any
. Is that documented in this PR?