Template literal types and mapped type 'as' clauses by ahejlsberg · Pull Request #40336 · microsoft/TypeScript (original) (raw)
This PR implements two new features:
- Template literal types, which are a form of string literals with embedded generic placeholders that can be substituted with actual string literals through type instantiation, and
- Mapped type
as
clauses, which provide the ability to transform property names in mapped types.
Template literal types
Template literal types are the type space equivalent of template literal expressions. Similar to template literal expressions, template literal types are enclosed in backtick delimiters and can contain placeholders of the form ${T}
, where T
is a type that is assignable to string
, number
, boolean
, or bigint
. Template literal types provide the ability to concatenate literal strings, convert literals of non-string primitive types to their string representation, and change the capitalization or casing of string literals. Furthermore, through type inference, template literal types provide a simple form of string pattern matching and decomposition.
Template literal types are resolved as follows:
- Union types in placeholders are distributed over the template literal type. For example
`[${A|B|C}]`
resolves to`[${A}]` | `[${B}]` | `[${C}]`
. Union types in multiple placeholders resolve to the cross product. For example`[${A|B},${C|D}]`
resolves to`[${A},${C}]` | `[${A},${D}]` | `[${B},${C}]` | `[${B},${D}]`
. - String, number, boolean, and bigint literal types in placeholders cause the placeholder to be replaced with the string representation of the literal type. For example
`[${'abc'}]`
resolves to`[abc]`
and`[${42}]`
resolves to`[42]`
. - Any one of the types
any
,string
,number
,boolean
, orbigint
in a placeholder causes the template literal to resolve to typestring
. - The type
never
type in a placeholder causes the template literal to resolve tonever
.
Some examples:
type EventName = ${T}Changed
;
type Concat<S1 extends string, S2 extends string> = ${S1}${S2}
;
type ToString<T extends string | number | boolean | bigint> = ${T}
;
type T0 = EventName<'foo'>; // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>; // 'fooChanged' | 'barChanged' | 'bazChanged'
type T2 = Concat<'Hello', 'World'>; // 'HelloWorld'
type T3 = ${'top' | 'bottom'}-${'left' | 'right'}
; // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
type T4 = ToString<'abc' | 42 | true | -1234n>; // 'abc' | '42' | 'true' | '-1234'
Beware that the cross product distribution of union types can quickly escalate into very large and costly types. Also note that union types are limited to less than 100,000 constituents, and the following will cause an error:
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = ${Digit}${Digit}${Digit}${Digit}${Digit}
; // Error
A template literal placeholder may optionally specify an uppercase
, lowercase
, capitalize
, or uncapitalize
modifier before the type. This modifier changes the casing of the entire replacement string or the first character of the replacement string. For example:
EDIT: Based on feedback, the casing modifiers have been replaced by intrinsic string types in #40580.
type GetterName = get${Capitalize<T>}
;
type Cases = ${Uppercase<T>} <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>L</mi><mi>o</mi><mi>w</mi><mi>e</mi><mi>r</mi><mi>c</mi><mi>a</mi><mi>s</mi><mi>e</mi><mo><</mo><mi>T</mi><mo>></mo></mrow><annotation encoding="application/x-tex">{Lowercase<T>} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7224em;vertical-align:-0.0391em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">erc</span><span class="mord mathnormal">a</span><span class="mord mathnormal">se</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel"><</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">></span></span></span></span></span>{Capitalize<T>} ${Uncapitalize<T>}
;
type T10 = GetterName<'foo'>; // 'getFoo'
type T11 = Cases<'bar'>; // 'BAR bar Bar bar'
type T12 = Cases<'BAR'>; // 'BAR bar BAR bAR'
Template literal types are all assignable to and subtypes of string
. Furthermore, a template literal type `${T}`
is assignable to and a subtype of a template literal type `${C}`
, where C
is a string literal type constraint of T
. For example:
function test<T extends 'foo' | 'bar'>(name: get${Capitalize<T>}
) {
let s1: string = name;
let s2: 'getFoo' | 'getBar' = name;
}
Type inference supports inferring from a string literal type to a template literal type. For inference to succeed the starting and ending literal character spans (if any) of the target must exactly match the starting and ending spans of the source. Inference proceeds by matching each placeholder to a substring in the source from left to right: A placeholder followed by a literal character span is matched by inferring zero or more characters from the source until the first occurrence of that literal character span in the source. A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.
Some examples:
type MatchPair = S extends [${infer A},${infer B}]
? [A, B] : unknown;
type T20 = MatchPair<'[1,2]'>; // ['1', '2'] type T21 = MatchPair<'[foo,bar]'>; // ['foo', 'bar'] type T22 = MatchPair<' [1,2]'>; // unknown type T23 = MatchPair<'[123]'>; // unknown type T24 = MatchPair<'[1,2,3,4]'>; // ['1', '2,3,4']
type FirstTwoAndRest = S extends ${infer A}${infer B}${infer R}
? [${A}${B}
, R] : unknown;
type T25 = FirstTwoAndRest<'abcde'>; // ['ab', 'cde'] type T26 = FirstTwoAndRest<'ab'>; // ['ab', ''] type T27 = FirstTwoAndRest<'a'>; // unknown
Template literal types can be combined with recursive conditional types to write Join
and Split
types that iterate over repeated patterns.
type Join<T extends unknown[], D extends string> =
T extends [] ? '' :
T extends [string | number | boolean | bigint] ? ${T[0]}
:
T extends [string | number | boolean | bigint, ...infer U] ? ${T[0]}${D}${Join<U, D>}
:
string;
type T30 = Join<[1, 2, 3, 4], '.'>; // '1.2.3.4'
type T31 = Join<['foo', 'bar', 'baz'], '-'>; // 'foo-bar-baz'
type T32 = Join<[], '.'>; // ''
type Split<S extends string, D extends string> =
string extends S ? string[] :
S extends '' ? [] :
S extends ${infer T}${D}${infer U}
? [T, ...Split<U, D>] :
[S];
type T40 = Split<'foo', '.'>; // ['foo'] type T41 = Split<'foo.bar.baz', '.'>; // ['foo', 'bar', 'baz'] type T42 = Split<'foo.bar', ''>; // ['f', 'o', 'o', '.', 'b', 'a', 'r'] type T43 = Split<any, '.'>; // string[]
The recursive inference capabilities can for example be used to strongly type functions that access properties using "dotted paths", and pattern that is sometimes used in JavaScript frameworks.
type PropType<T, Path extends string> =
string extends Path ? unknown :
Path extends keyof T ? T[Path] :
Path extends ${infer K}.${infer R}
? K extends keyof T ? PropType<T[K], R> : unknown :
unknown;
declare function getPropValue<T, P extends string>(obj: T, path: P): PropType<T, P>; declare const s: string;
const obj = { a: { b: {c: 42, d: 'hello' }}}; getPropValue(obj, 'a'); // { b: {c: number, d: string } } getPropValue(obj, 'a.b'); // {c: number, d: string } getPropValue(obj, 'a.b.d'); // string getPropValue(obj, 'a.b.x'); // unknown getPropValue(obj, s); // unknown
Mapped type as
clauses
With this PR, mapped types support an optional as
clause through which a transformation of the generated property names can be specified:
where N
must be a type that is assignable to string | number | symbol
. Typically, N
is a type that transforms P
, such as a template literal type that uses P
in a placeholder. For example:
type Getters = { [P in keyof T & string as get${Capitalize<P>}
]: () => T[P] };
type T50 = Getters<{ foo: string, bar: number }>; // { getFoo: () => string, getBar: () => number }
Above, a keyof T & string
intersection is required because keyof T
could contain symbol
types that cannot be transformed using template literal types.
When the type specified in an as
clause resolves to never
, no property is generated for that key. Thus, an as
clause can be used as a filter:
type Methods = { [P in keyof T as T[P] extends Function ? P : never]: T[P] }; type T60 = Methods<{ foo(): number, bar: boolean }>; // { foo(): number }
When the type specified in an as
clause resolves to a union of literal types, multiple properties with the same type are generated:
type DoubleProp = { [P in keyof T & string as ${P}1
| ${P}2
]: T[P] }
type T70 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b1: number, b2: number }
Fixes #12754.
Playground: https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88