📝

TypeScript Template Literal Types 公式ドキュメント

に公開

はじめに

TypeScript の Template Literal Types(テンプレートリテラル型) は、文字列リテラル型に基づき、Union を介して多くの文字列に展開できる機能です。

TypeScript 公式ドキュメントの内容に基づき、Template Literal Types の基本概念から応用的な使用方法まで、コード例とともに解説します。

Template Literal Types

Template Literal Types は文字列リテラル型に基づいて構築され、Union を介して多くの文字列に展開する能力を持ちます。

JavaScript のテンプレートリテラル文字列と同じ構文を持ちますが、型の位置で使用されます。具体的なリテラル型と一緒に使用すると、テンプレートリテラルは内容を連結して新しい文字列リテラル型を生成します。

type World = "world";

type Greeting = `hello ${World}`;
//   ^? type Greeting = "hello world"

補間位置で Union が使用されると、その型は各 Union メンバーによって表現される可能性のあるすべての文字列リテラルのセットになります:

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
//   ^? type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

テンプレートリテラルの各補間位置に対して、Union は交差乗算されます:

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";

type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
//   ^? type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" |
//                              "en_footer_title_id" | "en_footer_sendoff_id" |
//                              "ja_welcome_email_id" | "ja_email_heading_id" |
//                              "ja_footer_title_id" | "ja_footer_sendoff_id" |
//                              "pt_welcome_email_id" | "pt_email_heading_id" |
//                              "pt_footer_title_id" | "pt_footer_sendoff_id"

一般的に、大きな文字列 Union については事前生成を推奨しますが、小さなケースでは有用です。

String Unions in Types

Template Literal の力は、型内の情報に基づいて新しい文字列を定義することにあります。

関数(makeWatchedObject)が渡されたオブジェクトに on() という新しい関数を追加するケースを考えてみましょう。JavaScript では、その呼び出しは makeWatchedObject(baseObject) のように見えるかもしれません。ベースオブジェクトは次のようになると想像できます:

const passedObject = {
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
};

ベースオブジェクトに追加される on 関数は、2 つの引数、eventNamestring)と callbackfunction)を期待します。

eventNameattributeInThePassedObject + "Changed" の形式である必要があります。つまり、ベースオブジェクトの属性 firstName から派生した firstNameChanged です。

コールバック関数が呼び出されるとき:

  • attributeInThePassedObject という名前に関連付けられた型の値を渡される必要があります。つまり、firstNamestring として型付けされているため、firstNameChanged イベントのコールバックは呼び出し時に string が渡されることを期待します。同様に、age に関連するイベントは number 引数で呼び出されることを期待するはずです
  • void 戻り値型を持つ必要があります(簡単な実演のため)

on() の素朴な関数シグネチャは on(eventName: string, callback: (newValue: any) => void) のようになるかもしれません。しかし、前述の説明で、コードに文書化したい重要な型制約を特定しました。Template Literal Types により、これらの制約をコードに取り込むことができます。

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});

// makeWatchedObject は匿名オブジェクトに `on` を追加しました
person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

on"firstNameChanged" イベントをリッスンしており、単なる "firstName" ではないことに注意してください。on() の素朴な仕様は、適格なイベント名のセットが監視されるオブジェクト内の属性名の Union と末尾に追加された "Changed" によって制約されることを保証するとより堅牢になります。JavaScript では Object.keys(passedObject).map(x => ${x}Changed) のような計算を行うことに慣れていますが、型システム内のテンプレートリテラルは文字列操作に似たアプローチを提供します:

type PropEventSource<Type> = {
  on(
    eventName: `${string & keyof Type}Changed`,
    callback: (newValue: any) => void
  ): void;
};

/// `on` メソッドを持つ "watched object" を作成し、
/// プロパティの変更を監視できるようにします。
declare function makeWatchedObject<Type>(
  obj: Type
): Type & PropEventSource<Type>;

これにより、間違ったプロパティが与えられたときにエラーが発生するものを構築できます:

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});

person.on("firstNameChanged", () => {});

// 簡単な人的エラーを防ぐ(イベント名の代わりにキーを使用)
person.on("firstName", () => {});
// ❌ Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

// タイポに対応
person.on("frstNameChanged", () => {});
// ❌ Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

Inference with Template Literals

元の渡されたオブジェクトで提供されたすべての情報の恩恵を受けていないことに注意してください。firstName の変更(つまり firstNameChanged イベント)を考えると、コールバックは string 型の引数を受け取ることを期待するはずです。同様に、age の変更に対するコールバックは number 引数を受け取るはずです。callback の引数の型付けに素朴に any を使用しています。再び、Template Literal Types により、属性のデータ型がその属性のコールバックの最初の引数と同じ型になることを保証できます。

これを可能にする重要な洞察は次のとおりです:次のようなジェネリックを持つ関数を使用できます:

  1. 最初の引数で使用されるリテラルがリテラル型としてキャプチャされる
  2. そのリテラル型は、ジェネリック内の有効な属性の Union にあることが検証できる
  3. 検証された属性の型は、Indexed Access を使用してジェネリックの構造で検索できる
  4. この型情報を適用して、コールバック関数への引数が同じ型であることを保証できる
type PropEventSource<Type> = {
  on<Key extends string & keyof Type>(
    eventName: `${Key}Changed`,
    callback: (newValue: Type[Key]) => void
  ): void;
};

declare function makeWatchedObject<Type>(
  obj: Type
): Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});

person.on("firstNameChanged", (newName) => {
  // ^? (parameter) newName: string
  console.log(`new name is ${newName.toUpperCase()}`);
});

person.on("ageChanged", (newAge) => {
  // ^? (parameter) newAge: number
  if (newAge < 0) {
    console.warn("warning! negative age");
  }
});

ここで、on をジェネリックメソッドにしました。

ユーザーが文字列 "firstNameChanged" で呼び出すと、TypeScript は Key の適切な型を推論しようとします。それを行うために、"Changed" の前のコンテンツに対して Key をマッチさせ、文字列 "firstName" を推論します。TypeScript がそれを理解すると、on メソッドは元のオブジェクト上の firstName の型を取得でき、この場合は string です。同様に、"ageChanged" で呼び出されると、TypeScript はプロパティ age の型を見つけ、それは number です。

推論はさまざまな方法で組み合わせることができ、多くの場合、文字列を分解し、それらを異なる方法で再構築します。

Intrinsic String Manipulation Types

文字列操作を支援するため、TypeScript には文字列操作で使用できる型のセットが含まれています。これらの型はパフォーマンスのためにコンパイラに組み込まれており、TypeScript に含まれる .d.ts ファイルでは見つけることができません。

Uppercase<StringType>

文字列内の各文字を大文字版に変換します。

Example

type Greeting = "Hello, world";
type ShoutyGreeting = Uppercase<Greeting>;
//   ^? type ShoutyGreeting = "HELLO, WORLD"

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<"my_app">;
//   ^? type MainID = "ID-MY_APP"

Lowercase<StringType>

文字列内の各文字を小文字相当に変換します。

Example

type Greeting = "Hello, world";
type QuietGreeting = Lowercase<Greeting>;
//   ^? type QuietGreeting = "hello, world"

type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`;
type MainID = ASCIICacheKey<"MY_APP">;
//   ^? type MainID = "id-my_app"

Capitalize<StringType>

文字列内の最初の文字を大文字相当に変換します。

Example

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
//   ^? type Greeting = "Hello, world"

Uncapitalize<StringType>

文字列内の最初の文字を小文字相当に変換します。

Example

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
//   ^? type UncomfortableGreeting = "hELLO WORLD"

まとめ

TypeScript の Template Literal Types を使用することで、文字列型レベルでの操作や検証を実現できます。

公式ドキュメントで紹介されている主要な概念:

  1. 基本的な Template Literal: バッククォートを使用した文字列型の生成
  2. String Unions in Types: 型内の情報に基づく新しい文字列の定義
  3. Inference with Template Literals: テンプレートリテラルでの型推論
  4. Intrinsic String Manipulation Types: 組み込みの文字列操作型

重要なポイント

  • Template Literal Types は文字列リテラル型を基に多くの文字列に展開可能
  • Union 型との組み合わせで複雑な文字列パターンを生成
  • 型推論と組み合わせて型安全な API を構築
  • 組み込みの文字列操作型で大文字・小文字変換が可能

この機能により、文字列型レベルでの高度な操作を実現し、より型安全で表現力豊かな TypeScript コードを書けるようになります。

参考リンク

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

Discussion