Kodokon kodokon.com

テンプレートリテラル型: コンパイル時に計算される文字列

テンプレートリテラル型を活用して直積によってユニオンを生成し、infer で文字列リテラルを解析する。

9 分 · 3 問

このレッスンを Kodokon で開く

テンプレートリテラル型は、テンプレートリテラルの構文を型の世界に持ち込みます。ユニオンを補間すると、コンパイラは直積を計算します: 可能なすべての組み合わせが結果のメンバーになります。これは、イベントキー、ルート、デザインシステムのトークンを 1 文字単位まで正確に型付けできるツールです。

TYPESCRIPT
type Lang = "fr" | "en";
type Theme = "light" | "dark";

type ThemeKey = `${Lang}-${Theme}`;
// "fr-light" | "fr-dark" | "en-light" | "en-dark"
補間された 2 つのユニオン: 4 メンバーの直積。

その真価は infer と組み合わせたときに現れます: テンプレートリテラル型は文字列に対するマッチングパターンになります。コンパイラは各セグメントに一致するよう入力リテラルを切り分けます。複数の分割が可能な場合は、区切り文字の最初の出現が勝ちます。再帰を加えれば、コンパイル時に評価される本格的なパーサーが得られます。

TYPESCRIPT
type Split<S extends string, Sep extends string> =
  S extends `${infer Head}${Sep}${infer Rest}`
    ? [Head, ...Split<Rest, Sep>]
    : [S];

type Parts = Split<"a.b.c", ".">;
// ["a", "b", "c"]
完全に静的な再帰的分割。

このパターンは、Express スタイルのルートのパラメータを抽出するときに輝きます。各再帰呼び出しが 1 つの :param セグメントを消費し、最終的なユニオンがすべてのパラメータ名を列挙します。それを Record に渡してハンドラのシグネチャを型付けできます。

TYPESCRIPT
type Params<Path extends string> =
  Path extends `${string}:${infer P}/${infer Rest}`
    ? P | Params<Rest>
    : Path extends `${string}:${infer P}`
      ? P
      : never;

type RouteParams = Params<"/users/:id/posts/:postId">;
// "id" | "postId"
コンパイル時にルートのパラメータを抽出する。

TypeScript は 4 つの組み込み (intrinsic) ユーティリティ - UppercaseLowercaseCapitalizeUncapitalize - を提供しており、これらはコンパイラ内部で直接実装されているため(intrinsic キーワード)、自分で書き直すことはできません。もう一つの実運用パターン: string & {}。ユニオンの中では、string がリテラルを吸収してしまい、エディタは補完を失います。{} と交差させると、等価だが別個の型が作られ、コンパイラはそれを縮約しません。

TYPESCRIPT
type Method = "get" | "post";
type Handler = `on${Capitalize<Method>}`;
// "onGet" | "onPost"

type KnownColor = "red" | "green" | "blue";
type Color = KnownColor | (string & {});

declare function paint(color: Color): void;

paint("red");     // suggested by autocompletion
paint("#ff8800"); // also accepted
組み込みユーティリティと、補完のための string & {} テクニック。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. A が 3 メンバー、B が 4 メンバーを持つ場合、${A}-${B} は何メンバーを含みますか?
    • 7
    • 12
    • 1
    • 4
  2. Capitalize をリテラル "hello world" に適用すると何が生成されますか?
    • "Hello World"
    • "Hello world"
    • "HELLO WORLD"
  3. なぜ KnownColor | string ではなく KnownColor | (string & {}) と書くのですか?
    • KnownColor 以外の文字列を禁止するため
    • string がリテラルを吸収するのを防ぎ、補完を維持するため
    • コンパイラの性能を向上させるため
    • 2 つの形は厳密に等価である