【TypeScript 4.3 Beta】Template Literal Types

5 min read読了の目安(約4800字

公式ブログで TypeScript 4.3 Beta が発表されました。
この記事では、この内の Template Literal Types だけ取り上げて、どのような変更があったのかを書いていきたいと思います。

その他の変更点については、こちらの記事で取り上げています。

Template String Type Improvements

Template Literal Types が具体的に以下の2点で改善されました。

  1. 文脈的に Template Literal Types かどうか判断されるようになった
  2. 異なる Template Literal Types 間の関係と推論が改善された

そもそも Template Literal Types ってなんだっけ?

JavaScript では、テンプレートリテラルを使って、以下のように変数などの式を文字列内に埋め込むことができます。

テンプレートリテラル
const name = 'aki';  
console.log(`Hello, ${name}!`) // Hello, aki! 

v4.1 に新しく導入された Template Literal Types は、JavaScript のテンプレートリテラルと同じ構文で、型を生成することができます。

Template Literal Types
type Animal = "shark" | "giraffe" | "platypus";
type BabyAnimal = `baby-${Animal}`; // "baby-shark" | "baby-giraffe" | "baby-platypus"

上記のように、リテラル型と合わせて使うことで、内容を結合して新しい文字列リテラル型を生成することができます。

TypeScript 4.2 Beta では

v4.2 Beta でテンプレートリテラルを書いたとき、その型は Template Literal Types という扱いになっていました。

v4.2 Beta
const n: number = 123;

// value1 は `${number}px` 型
const value1 = `${n}px`;  

// value2 は string 型
let value2 = `${n}px`;  

ただし、上記の value2 のように、let に代入する場合は Template Literal Types ではなく string として扱われます。let は再代入可能なので、型を広げる ( 公式ブログでは widening という表現を使っていた ) ためだと思われます。

TypeScript 4.2 では

しかし、v4.2 で revert されることになりました。

テンプレートリテラルを const で定義した場合、常に Template Literal Types になるというのは望ましくないケースもあるということで、デフォルトでは string として扱われることになりました。

v4.2
const n: number = 123;

// value1 は string 型
const value1 = `${n}px`;  

// value2 は string 型
let value2 = `${n}px`; 

// value3 は `${number}px` 型
const value3 = `${n}px` as const; 

その代わり value3 のように、末尾に as constをつけた場合は Template Literal Types になるように変更されました。

1. 文脈的にTemplate Literal Typesかどうか判断されるようになった

以上を踏まえて、v4.3 Beta では Template Literal Types がどのように変更されたか見ていきたいと思います。

v4.3 Beta 以前

v4.2 では as const をつけない限り Template Literal Types にはならないので、以下のコードはエラーになっていました。

v4.2
function bar(s: string): `hello ${string}` {
    // Error! Type 'string' is not assignable to type '`hello ${string}`'.
    return `hello ${s}`;
}

v4.3 Beta 以降

v4.3 Beta では、「文脈的に Template Literal Types しかありえないよね」という場合 ( 公式ブログでは contextually typed という表現 ) には、as const が無くても Template Literal Types が適応されるようになりました。

なので先ほどはエラーだったこちらのコードも、v4.3 Beta では問題なく通るようになります。

v4.3 Beta
function bar(s: string): `hello ${string}` {
    // OK! \(^o^)/
    return `hello ${s}`;
}

【 Playgroundで確認する 】 contextuallyすごい 👏

2. 異なる Template Literal Types 間の関係と推論が改善された

v4.3 Beta 以前

今までは、target 側 が Template Literal Types であるとき、source 側も Template Literal Types にすることはできませんでした。

以下の例では、s1 が target 側、s2 が source 側になります。

v4.3 Beta 以前
declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`; 

s1 = s2;

s2 は バッククォートで囲まれているので、一見 Template Literal Types に見えますが、マウスカーソルを合わせると、実際には文字列リテラル型であると分かります。

文字列リテラル型の値を Template Literal Types の変数に代入することは可能なので、上記のコードは問題なく通ります。

しかし、target 側 (s1) も source 側 (s3) も両方 Template Literal Types である場合は、以下のようにエラーになります。

v4.3 Beta 以前
declare let s1: `${number}-${number}-${number}`;
declare let s3: `${number}-2-3`;

s1 = s3; // Error! Type '`${number}-2-3`' is not assignable to type '`${number}-${number}-${number}`'.

v4.3 Beta 以降

v4.3 Beta では、target 側と source 側で両方とも Template Literal Types だったとしても、怒られなくなりました。

TypeScript が互換性があるかどうか判断できるようになったため、以下のように ${...} を自由に組み合わせることもできます。

v4.3 Beta
declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
declare let s4: `1-${number}-3`;
declare let s5: `1-2-${number}`;
declare let s6: `${number}-2-${number}`;

// すべてOK! \(^o^)/
s1 = s2;
s1 = s3;
s1 = s4;
s1 = s5;
s1 = s6;

【 Playgroundで確認する 】

参考