【TypeScript】リテラル型で値を限定する

JavaScript

こんにちは、かつコーチです。

前回はunion型とintersection型を使って、複数の型を組み合わせる方法を解説しました。

その中で何度か登場した「リテラル型」について、今回はあらためて詳しく掘り下げていきます。

「特定の値しか受け付けない型」を作れるようになると、想定外の値の混入をコンパイル時に防げるようになり、コードの安全性が大きく向上します。

リテラル型とは?

通常の型との違い

リテラル型とは、stringnumber のような広い型ではなく、「特定の値そのもの」を型として扱う仕組みです。

let status1: string = "success"; // stringならどんな文字列でもOK
let status2: "success" = "success"; // "success"という値しか受け付けない

status2 = "error"; // エラー:"success"以外は代入できない

string 型は「文字列であれば何でも入る」という広い型ですが、"success" のようにダブルクォートで値そのものを型として書くと、その値以外は代入できなくなります。

数値や真偽値でも同様にリテラル型を作れます。

let count: 1 = 1;
let isActive: true = true;

リテラル型の使いどころ

union型と組み合わせて選択肢を限定する

リテラル型は単体で使うよりも、union型と組み合わせて「決まった選択肢の中の一つ」を表現する場面でよく使われます。

type Direction = "up" | "down" | "left" | "right";

function move(direction: Direction): void {
  console.log(`${direction}に移動します`);
}

move("up"); // OK
move("diagonal"); // エラー:Direction型に含まれない値

Direction 型に含まれる4つの文字列以外は代入できないため、タイプミスや仕様外の値が紛れ込むのをコンパイル時に防げます。

string 型で受け取っていた場合は、"upp" のような単純なタイプミスも実行時までエラーに気づけませんが、リテラル型のunionならその場で分かります。

定数のオブジェクトから型を作る

選択肢を毎回リテラル型として手書きするのではなく、実際の値の一覧(オブジェクトや配列)から型を自動生成する方法もよく使われます。

const STATUS = {
  LOADING: "loading",
  SUCCESS: "success",
  ERROR: "error",
} as const;

type Status = (typeof STATUS)[keyof typeof STATUS];
// Status型は "loading" | "success" | "error" と同じ意味になる

function showMessage(status: Status): string {
  switch (status) {
    case STATUS.LOADING:
      return "読み込み中です";
    case STATUS.SUCCESS:
      return "成功しました";
    case STATUS.ERROR:
      return "エラーが発生しました";
  }
}

console.log(showMessage(STATUS.SUCCESS)); // "成功しました"

as const をつけることで、STATUS オブジェクトの各プロパティの値が string ではなく、それぞれのリテラル型として扱われるようになります。

typeof STATUS でオブジェクトの型を取得し、keyof でそのキー一覧を取り、さらにインデックスアクセス([keyof typeof STATUS])で値の型一覧を取り出す、という組み合わせです。

少しテクニカルですが、「実際に使う値の一覧」と「その値を表す型」を一箇所で管理できるようになるため、値の追加・変更に強い書き方です。

as const の役割をもう少し詳しく

as const をつけない場合との違い

as const がどのように効いているのか、比較して確認してみましょう。

const obj1 = { status: "success" };
// objの型は { status: string } と推論される(広い型)

const obj2 = { status: "success" } as const;
// obj2の型は { readonly status: "success" } と推論される(狭いリテラル型)

as const をつけないと、TypeScriptは「今後この値は変わるかもしれない」という前提で、プロパティの型を広い string として推論します。

as const をつけると、「この値はこれ以上変わらない」という意図が伝わり、プロパティの型がそのリテラル型に固定され、さらに readonly も自動的につきます。

「値を限定したい」という意図がある場面では、この as const を活用できるかどうかが大きな分かれ目になります。

つまずきやすいポイント:配列を渡したらリテラル型が広がってしまった

実際につまずいた話

私が as const を覚えたてのころ、配列の要素をそのままリテラル型のunionとして使おうとして、なかなかうまくいかない経験をしました。

やりたかったのは、許可するロール(役割)の一覧を配列で定義し、そこから型を自動生成することでした。

最初は as const をつけずに配列を定義してしまい、期待した「限定されたリテラル型のunion」ではなく、ただの string[] 型になってしまうという事態にぶつかりました。

型エラーが出るべき場面でエラーが出ず、「あれ、リテラル型になっていない」と気づくまでに少し時間がかかったのを覚えています。

原因は単純で、配列リテラルに as const をつけ忘れていたことでした。

Before/Afterで確認する

❌ Before:as constをつけずに配列から型を作ろうとする

const ROLES = ["admin", "editor", "viewer"];
// as constがないため、ROLESの型はstring[]と推論される

type Role = (typeof ROLES)[number];
// Role型はstring型になってしまい、限定できていない

function checkRole(role: Role): void {
  console.log(`${role}権限で処理します`);
}

checkRole("guest"); // 本来は許可していない値だが、コンパイルエラーにならない

ROLESas const がついていないため、配列の型は string[] と推論されてしまい、Role 型も結局ただの string になります。

その結果、"admin""editor" 以外の任意の文字列も受け入れてしまいます。

✅ After:as constをつけてリテラル型のunionを作る

const ROLES = ["admin", "editor", "viewer"] as const;
// ROLESの型は readonly ["admin", "editor", "viewer"] になる

type Role = (typeof ROLES)[number];
// Role型は "admin" | "editor" | "viewer" になる

function checkRole(role: Role): void {
  console.log(`${role}権限で処理します`);
}

checkRole("admin"); // OK
checkRole("guest"); // エラー:Role型に含まれない値

as const を配列につけることで、ROLES の型が「読み取り専用のタプル」として固定され、(typeof ROLES)[number] で取り出した Role 型も正しくリテラル型のunionになります。

配列や設定値から型を自動生成したいときは、as const を忘れていないか必ず確認する習慣をつけておくと、このつまずきを避けられます。

応用:テンプレートリテラル型で文字列パターンを表現する

決まったパターンの文字列を型で表現する

リテラル型は、テンプレートリテラルと組み合わせることで、より柔軟なパターンを表現できます。

type Size = "sm" | "md" | "lg";
type Color = "red" | "blue";

type ButtonClass = `btn-${Size}-${Color}`;
// "btn-sm-red" | "btn-sm-blue" | "btn-md-red" | ... のすべての組み合わせになる

const className: ButtonClass = "btn-md-blue"; // OK
const invalidClassName: ButtonClass = "btn-xl-blue"; // エラー:"xl"はSize型に含まれない

`btn-${Size}-${Color}` のようにバッククォートを使ったテンプレートリテラル型を書くと、SizeColor のすべての組み合わせを網羅したリテラル型のunionが自動的に作られます。

CSSのクラス名やAPIのエンドポイント文字列のように、決まったパターンで組み合わさる文字列を扱うときに役立つテクニックです。

まとめ

この記事のポイント

  • リテラル型は「特定の値そのもの」を型として扱う仕組み
  • union型と組み合わせることで、決まった選択肢の中の一つに値を限定できる
  • as const をつけることで、値の型を広い型ではなくリテラル型として固定できる
  • 配列や定数オブジェクトから型を自動生成する際は as const の付け忘れに注意する
  • テンプレートリテラル型を使うと、決まったパターンの文字列を型として表現できる

次に読むべき記事

リテラル型まで理解できたら、次は typeinterface の違いと使い分けを学んでいきましょう。

→ 次の記事:typeとinterface、結局どちらを使うべき?

タイトルとURLをコピーしました