TypeScript 速習チュートリアル

TypeScript リテラル型

1. TypeScript における Literal Types の理解

TypeScript の Literal Types(リテラル型) は、変数に代入できる「正確な値」を直接指定できる機能です。これは stringnumber といった抽象的な型よりもはるかに高い精度を提供します。
これらは、精密で型安全なアプリケーションを構築するための構成要素となります。

1.1 主要なコンセプト

  • String Literals(文字列リテラル): "success" | "error" のような特定の文字列値
  • Numeric Literals(数値リテラル): 1 | 2 | 3 のような特定の数値
  • Boolean Literals(真偽値リテラル): true または false
  • Template Literal Types(テンプレートリテラル型): テンプレート文字列の構文を使用して構築される文字列リテラル型

1.2 一般的なユースケース

  • 許可される値のセットを厳密に定義する
  • Discriminated Unions(識別子付き共用体)の作成
  • 型安全なイベントハンドリング
  • API レスポンスの型定義
  • コンフィギュレーションオブジェクト

2. String Literal Types

文字列リテラル型は、特定の文字列値を表します。

コード例

// 文字列リテラル型の変数
let direction: "north" | "south" | "east" | "west";

// 有効な代入
direction = "north";
direction = "south";

// 無効な代入はエラーを引き起こします
// direction = "northeast"; // エラー: 型 '"northeast"' を型 '"north" | "south" | "east" | "west"' に割り当てることはできません
// direction = "up"; // エラー: 型 '"up"' を型 '"north" | "south" | "east" | "west"' に割り当てることはできません

// 関数で文字列リテラル型を使用する
function move(direction: "north" | "south" | "east" | "west") {
  console.log(`${direction} に移動中`);
}

move("east"); // 有効
// move("up"); // エラー: 型 '"up"' の引数は型 '"north" | "south" | "east" | "west"' のパラメータに割り当てることはできません...

3. Numeric Literal Types

文字列リテラルと同様に、数値リテラル型は特定の数値を表します。

コード例

// 数値リテラル型の変数
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;

// 有効な代入
diceRoll = 1;
diceRoll = 6;

// 無効な代入はエラーを引き起こします
// diceRoll = 0; // エラー: 型 '0' は型 '1 | 2 | 3 | 4 | 5 | 6' に割り当てることはできません
// diceRoll = 7; // エラー: 型 '7' は型 '1 | 2 | 3 | 4 | 5 | 6' に割り当てることはできません
// diceRoll = 2.5; // エラー: 型 '2.5' は型 '1 | 2 | 3 | 4 | 5 | 6' に割り当てることはできません

// 関数で数値リテラル型を使用する
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
  return (Math.floor(Math.random() * 6) + 1) as 1 | 2 | 3 | 4 | 5 | 6;
}

const result = rollDice();
console.log(`出目: ${result}`);

4. Boolean Literal Types

真偽値リテラル型は、真偽値が2つしかないため一般的にはあまり使われませんが、特定のシナリオでは有用です。

コード例

// リテラル値 'true' のみを保持できる型
type YesOnly = true;

// 常に true を返さなければならない関数
function alwaysSucceed(): true {
  // 常にリテラル値 'true' を返す
  return true;
}

// 他の型と組み合わせた真偽値リテラル
type SuccessFlag = true | "success" | 1;
type FailureFlag = false | "failure" | 0;

function processResult(result: SuccessFlag | FailureFlag) {
  if (result === true || result === "success" || result === 1) {
    console.log("操作に成功しました");
  } else {
    console.log("操作に失敗しました");
  }
}

processResult(true);      // "操作に成功しました"
processResult("success"); // "操作に成功しました"
processResult(1);         // "操作に成功しました"
processResult(false);     // "操作に失敗しました"

5. Literal Types with Objects

リテラル型をオブジェクト型と組み合わせることで、非常に具体的な形状(Shape)を作成できます。

コード例

// リテラルのプロパティ値を持つオブジェクト
type HTTPSuccess = {
  status: 200 | 201 | 204;
  statusText: "OK" | "Created" | "No Content";
  data: any;
};

type HTTPError = {
  status: 400 | 401 | 403 | 404 | 500;
  statusText: "Bad Request" | "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
  error: string;
};

type HTTPResponse = HTTPSuccess | HTTPError;

function handleResponse(response: HTTPResponse) {
  if (response.status >= 200 && response.status < 300) {
    console.log(`成功: ${response.statusText}`);
    console.log(response.data);
  } else {
    console.log(`エラー ${response.status}: ${response.statusText}`);
    console.log(`メッセージ: ${response.error}`);
  }
}

// 使用例
const successResponse: HTTPSuccess = {
  status: 200,
  statusText: "OK",
  data: { username: "john_doe", email: "[email protected]" }
};

const errorResponse: HTTPError = {
  status: 404,
  statusText: "Not Found",
  error: "データベースにユーザーが見つかりません"
};

handleResponse(successResponse);
handleResponse(errorResponse);

6. Template Literal Types

TypeScript 4.1 以降で導入された Template Literal Types は、テンプレート文字列の構文を使用して既存のリテラル型を組み合わせ、新しい文字列リテラル型を作成できます。

コード例

// 基本的なテンプレートリテラル
type Direction = "north" | "south" | "east" | "west";
type Distance = "1km" | "5km" | "10km";

// これらを組み合わせてテンプレートリテラルを使用する
type DirectionAndDistance = `${Direction}-${Distance}`;
// "north-1km" | "north-5km" | "north-10km" | "south-1km" | ...

let route: DirectionAndDistance;
route = "north-5km";  // 有効
route = "west-10km";   // 有効
// route = "north-2km"; // エラー
// route = "5km-north"; // エラー

// 高度な文字列操作
type EventType = "click" | "hover" | "scroll";
type EventTarget = "button" | "link" | "div";
type EventName = `on${Capitalize<EventType>}${Capitalize<EventTarget>}`;
// "onClickButton" | "onClickLink" | "onClickDiv" | ...

// 動的なプロパティアクセス
type User = {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
};

type GetterName<T> = `get${Capitalize<string & keyof T>}`;
type UserGetters = {
  [K in keyof User as GetterName<User>]: () => User[K];
};
// { getId: () => number; getName: () => string; ... }

// 文字列パターンマッチング
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
    : never;

type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"

// CSS の単位と値
type CssUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CssValue = `${number}${CssUnit}`;

let width: CssValue = '100px'; // 有効
let height: CssValue = '50%';  // 有効
// let margin: CssValue = '10'; // エラー
// let padding: CssValue = '2ex'; // エラー

// API バージョニング
type ApiVersion = 'v1' | 'v2' | 'v3';
type Endpoint = 'users' | 'products' | 'orders';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type ApiUrl = `https://api.example.com/${ApiVersion}/${Endpoint}`;

// 複雑な例: 動的な SQL クエリビルダー
type Table = 'users' | 'products' | 'orders';
type Column<T extends Table> =
  T extends 'users' ? 'id' | 'name' | 'email' | 'created_at' :
  T extends 'products' ? 'id' | 'name' | 'price' | 'in_stock' :
  T extends 'orders' ? 'id' | 'user_id' | 'total' | 'status' : never;

type WhereCondition<T extends Table> = {
  [K in Column<T>]?: {
    equals?: any;
    notEquals?: any;
    in?: any[];
  };
};

function query<T extends Table>(
  table: T,
  where?: WhereCondition<T>
): `SELECT * FROM ${T}${string}` {
  // 実装はクエリを構築する
  return `SELECT * FROM ${table}` as const;
}

// 使用例
const userQuery = query('users', {
  name: { equals: 'John' },
  created_at: { in: ['2023-01-01', '2023-12-31'] }
});
// 型: "SELECT * FROM users WHERE ..."

7. ベストプラクティス

リテラル型を効果的に使うための推奨事項(Do's and Don'ts)です。

7.1 推奨事項 (Do)

  • 固定された値のセット(Enum、コンフィギュレーションオプションなど)にはリテラル型を使用する
  • 型安全性を高めるために共用体型(Union Types)と組み合わせる
  • 文字列パターンマッチングにはテンプレートリテラル型を使用する
  • 可能な限り型推論を活用する
  • リテラル型の意味をドキュメント化する

7.2 非推奨事項 (Don't)

  • より一般的な型が適切な場合にリテラル型を過用しない
  • パフォーマンスを低下させるほど極端に大きな共用体型を作成しない
  • Enum がより適切な場面で文字列リテラルを使用しすぎない(ケースバイケース)

8. パフォーマンスに関する考慮事項

8.1 型チェックのパフォーマンス

  • 巨大な共用体型は型チェックを遅くする可能性があります
  • 複雑なテンプレートリテラル型はコンパイル時間を増大させる可能性があります
  • 複雑なリテラル型には型エイリアス(Type Aliases)の使用を検討してください
  • TypeScript の再帰深度の制限に注意してください