From 38798b2c01bc24dd1f0c61696752ae7d85b18cae Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Tue, 6 Mar 2018 22:25:40 -0800 Subject: [PATCH] Mapped Object Types --- doc/spec/Mapped Object Types.md | 159 ++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 doc/spec/Mapped Object Types.md diff --git a/doc/spec/Mapped Object Types.md b/doc/spec/Mapped Object Types.md new file mode 100644 index 00000000000..3be914f8abd --- /dev/null +++ b/doc/spec/Mapped Object Types.md @@ -0,0 +1,159 @@ + +# Mapped Object Types { #mapped-object-types } + +   `{` `readonly`opt `[`*Identifier* `in` *Type*`]` `?`opt `:` *Type* `}` + +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. + +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*. +* The *Identifier* forms a type variable *P* that is scoped only in *T*, and is bound and constrained to *K*. + +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. +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. + +```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. +`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. +Specifying a `readonly` modifier on a mapped type results in each property or index signature of the mapped object type becoming read-only. +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. +In the below example, `A1` is equivalent to `A2`, `B1` is equivalent to `B2`, and `C1` is equivalent to `C2`. + +```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. +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: + +```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*. + +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. +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; + readonly baz?: string; +} + +type B = { + [P in keyof T]?: T[P]; +} + +type C = { + [P in "foo" | "bar" | "baz"]?: T[P]; +} +``` + +## Generic mapped types + +A ***generic mapped type** is one whose constraint is a type parameter, a generic indexed access type, or a generic key query type, or a union containing any of the aforementiond types. + +Mapped object type syntax results in an object type with a series of different members depending on the operand type. + +* If *K* is the String type, then an object type is produced where *T* will be instantiated as *T'* in which instances of *P* in *T* have been substituted with *string*, and the produced object type contains a string index signature of type *T'* with the specified `readonly` and optionality modifiers. +* If *K* is declared as a key query operator `keyof` *O*, or as a type parameter whose constraint is a key query operator `keyof` *O*, then where the apparent type *O'* of *O*, + * if *O'* is the Any type, then an object type is produced where *T* will be instantiated as *T'* in which instances of *P* in *T* have been substituted with *string*, and the produced object type contains a string index signature of type *T'* with the specified `readonly` modifier. + * otherwise, an object type is produced with each property *P'* declared in *O'* with the type of *T'* in which instances of *P* in *T'* have been substituted with the string where *P* has *O'*`[`*P'*`]` with the same modifiers of *P'* in *O'*, as well as the specified `readonly` and optionality modifiers. +* If *K* is a string literal type, or a union of strictly string literal types, then an object type is produced where for each string literal constituent *S* of *K*, *T* will be instantiated as *T'* in which instances of *P* have been substituted with *S*, and the produced object type contains a property whose name is identical to the contents of *S*, and whose respective type is *T'*. +*TODO "named types" is the only place that talks about instantiation* \ No newline at end of file