Template string types and mapped type 'as' clauses 機能説明(意訳)

(これは以下の PR で Anders Hejlsberg 氏が最初にコメントした内容を意訳しただけの個人用メモです)

github.com

この PR は以下 2 つの新機能を実装します:

テンプレート文字列型

テンプレート文字列型はテンプレート文字列式の型版 (the type space equivalent of template string expressions) です。テンプレート文字列式と同様、テンプレート文字列型はバックティックで囲まれ、${T} の形のプレースホルダを含めることができます。ここで Tstring, number, boolean, または bigintアサイン可能な型です。テンプレート文字列型ではリテラル文字列を結合したり、非 string なプリミティブ型をその文字列表現に変換したり、文字列リテラルの大文字小文字を変えることができます。さらに、型推論によって、テンプレート文字列型はシンプルな形式の文字列パターンマッチと分解を行ってくれます。

テンプレート文字列型は以下のように解決されます:

  • プレースホルダ内の union 型はテンプレート文字列全体に分配されます。例えば `[${A|B|C}]``[${A}]` | `[${B}]` | `[${C}]` に解決します。複数のプレースホルダ内の union 型はクロス積に解決します。例えば `[${A|B},${C|D}]``[${A},${C}]` | `[${A},${D}]` | `[${B},${C}]` | `[${B},${D}]` に解決します。
  • プレースホルダ内の string, number, boolean, そして bigint リテラル型はそのリテラル型の文字列表現でプレースホルダを置き換えます。例えば `[${'abc'}]``[abc]` に、 `[${42}]``[42]` に解決します。
  • プレースホルダ内に any, string, number, boolean, または bigint 型のうちのいずれかが 1 つでもあれば、そのテンプレート文字列は string 型に解決します。
  • プレースホルダ内に never 型があれば、そのテンプレート文字列は never 型に解決します。

例をいくつか:

type EventName<T extends string> = `${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'

Union 型をクロス積分配するとあっという間に非常に大きくてコストのかかる型へと拡大する可能性があることに注意してください。また union 型の構成要素は 100,000 未満に制限されることにも注意してください。以下のコードはエラーになります:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`;  // Error

テンプレート文字列プレースホルダは型の前に uppercase, lowercase, capitalize, または uncapitalize 修飾子を任意で指定できます。この修飾子は置き換えられる文字列の全体または先頭文字の大文字小文字を変化させます。例えば:

type GetterName<T extends string> = `get${capitalize T}`;
type Cases<T extends string> = `${uppercase T} ${lowercase T} ${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'

テンプレート文字列型はすべて string 型とそのサブタイプにアサインできます。さらに、テンプレート文字列型 `${T}``${C}` (ここで CT の文字列リテラル型制約) とそのサブタイプにアサインできます。例えば:

function test<T extends 'foo' | 'bar'>(name: `get${capitalize T}`) {
    let s1: string = name;
    let s2: 'getFoo' | 'getBar' = name;
}

型推論では文字列リテラル型からテンプレート文字列型への推論をサポートしています。推論がうまくいくためには、推論先となるテンプレート文字列型の中の各プレースホルダが少なくとも1文字のスパンで隔てられていなければなりません(言い換えると、ぴったり隣り合ったプレースホルダたちへと推論することはできません)。各スパンを左から右へと非貪欲マッチを行いそのスパンで隔てられた2つの部分文字列からプレースホルダ型への推論を行うことで推論が進みます。例をいくつか:

type MatchPair<S extends string> = 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<'[1,2] '>;  // unknown
type T24 = MatchPair<'[1,2,3,4]'>;  // ['1', '2,3,4']

テンプレート文字列型を再帰的 conditional types と組み合わせて、繰り返しパターン上をイテレートする Join 型と Split 型を書くことができます。

type Join<T extends (string | number | boolean | bigint)[], D extends string> =
    T extends [] ? '' :
    T extends [unknown] ? `${T[0]}` :
    T extends [unknown, ...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 `${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<any, '.'>;  // string[]

再帰的推論機能は、例えば、プロパティにドット繋ぎでアクセスする関数や、 JavaScript フレームワークでたまに使われるパターンを強く型付けするのに使えます。

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

この PR により, mapped types はオプションの as 節をサポートします。 as 節を通して、生成されるプロパティ名の変換方法を指定できます:

{ [P in K as N]: X }

ここで Nstring 型にアサイン可能な型でなければなりません。 通常、N は、Pプレースホルダ内で利用するテンプレート文字列型のような、P を変換した型です。例えば:

type Getters<T> = { [P in keyof T & string as `get${capitalize P}`]: () => T[P] };
type T50 = Getters<{ foo: string, bar: number }>;  // { getFoo: () => string, getBar: () => number }

上記において、keyof T & string の交差が必要です。なぜなら keyof T はテンプレート文字列型を使って変換できない symbol 型を含むことがあるからです。

as 節内で指定された型が never に解決した場合、その key のプロパティは生成されません。したがって、as 節はフィルターとして使えます:

type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] };
type T60 = Methods<{ foo(): number, bar: boolean }>;  // { foo(): number }

as 節内で指定された型がリテラル型の union に解決した場合、同じ型の複数のプロパティが生成されます:

type DoubleProp<T> = { [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