【TypeScript】as constとリテラル型推論

JavaScript

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

前回はunion型を安全に絞り込む「型ガード」を扱いました。

今回はその型ガードとも相性がいい、as const(値を変更不可・かつ最も狭い型として扱う指示)とリテラル型推論を紹介します。

「配列や設定オブジェクトを定義したのに、思ったより型が広く推論されて困った」という経験がある人は、まさにこの記事のテーマにぶつかっています。

as constとは?

TypeScriptの型推論はデフォルトで”広い”

まず、as const をつけない場合にTypeScriptがどう型推論するかを見てみます。

const status = "success";
// 型は string ではなく "success"(変数宣言時点ではリテラル型)

let statusLet = "success";
// 型は string(letは再代入できるため広く推論される)

const で宣言した文字列はリテラル型として推論されますが、これはあくまで単純な代入の場合の話です。

オブジェクトや配列になると、事情が変わってきます。

const config = {
  status: "success",
  code: 200,
};
// config.status の型は string、config.code の型は number

config はオブジェクトなので、プロパティの値は「あとから書き換えられるかもしれない」という前提で、"success" ではなく string200 ではなく number という広い型に推論されます。

as constで型を”固定”する

as const を末尾につけると、この推論を「値そのまま」に固定できます。

const config = {
  status: "success",
  code: 200,
} as const;
// config.status の型は "success"、config.code の型は 200

すべてのプロパティが readonly になり、値もリテラル型として厳密に推論されるようになります。

「この値は絶対に変わらない設定情報だ」ということを、型のレベルでTypeScriptに伝えられるのが as const の役割です。

配列とas constの組み合わせ

タプル型として固定する

配列に as const をつけると、要素数や順序まで固定されたタプル型になります。

const rgb = [255, 0, 0];
// 型は number[](要素数も順序も保証されない)

const rgbConst = [255, 0, 0] as const;
// 型は readonly [255, 0, 0](要素数3、各要素の値まで固定)

as const なしの rgb は「数値がいくつか入った配列」としか型付けされませんが、as const をつけた rgbConst は「必ず3つの要素で、それぞれ255・0・0」という厳密な型になります。

定数配列からunion型を作る

as const の代表的な活用法が、配列からunion型を自動生成するテクニックです。

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number];
// type Role = "admin" | "editor" | "viewer"

function checkPermission(role: Role): void {
  console.log(`${role}の権限を確認します`);
}

checkPermission("admin"); // OK
checkPermission("owner"); // エラー:Argument of type '"owner"' is not assignable to parameter of type 'Role'.

typeof ROLES[number] は「ROLES 配列の要素の型」を取り出す書き方で、as const があるおかげで string ではなく "admin" | "editor" | "viewer" というunion型になります。

配列とtype定義が同じ場所にまとまるので、選択肢を追加するときに片方だけ直し忘れる、という事故も防げます。

つまずきやすいポイント:型が広がってエラーになる

定数オブジェクトをそのまま渡してハマった話

私が実際につまずいたのは、"success" | "error" を受け取る関数に、定数オブジェクトのプロパティを渡そうとしたときでした。

❌ Before:as constなしでオブジェクトを定義してエラーになった

type ResultStatus = "success" | "error";

function handleStatus(status: ResultStatus): void {
  console.log(`ステータス: ${status}`);
}

const apiResult = {
  status: "success",
  data: [1, 2, 3],
};

handleStatus(apiResult.status);
// エラー:Argument of type 'string' is not assignable to parameter of type 'ResultStatus'.

自分の目には apiResult.status が明らかに "success" という文字にしか見えていなかったので、なぜ string 扱いされてエラーになるのか最初は理解できませんでした。

原因は、オブジェクトのプロパティ値がデフォルトで string という広い型に推論されていたことでした。

✅ After:as constで値を固定してから渡す

type ResultStatus = "success" | "error";

function handleStatus(status: ResultStatus): void {
  console.log(`ステータス: ${status}`);
}

const apiResult = {
  status: "success",
  data: [1, 2, 3],
} as const;

handleStatus(apiResult.status); // OK:status は "success" というリテラル型

as const を1つ付け加えるだけで、apiResult.status の型が "success" に固定され、ResultStatus にそのまま渡せるようになりました。

この一件以来、「文字列や数値を決め打ちで扱う定数は、とりあえず as const をつけておく」という習慣がつきました。

応用・一歩先の使い方

satisfiesと組み合わせて安全性をさらに高める

as const は値を固定する一方、オブジェクトの形が正しいかまではチェックしてくれません。

そこで役立つのが satisfies 演算子です。

type Theme = {
  primary: string;
  secondary: string;
};

const theme = {
  primary: "#1a73e8",
  secondary: "#34a853",
} as const satisfies Theme;

console.log(theme.primary); // 型は "#1a73e8"(リテラル型のまま)

satisfies Theme を組み合わせることで、「Theme の形は満たしつつ、値の型はリテラル型のまま保つ」という両立が可能になります。

as Theme のようにキャストしてしまうと値の型が広がってしまいますが、satisfies ならその心配がありません。

enumの代替として使う

as const を使った定数オブジェクトは、enum の代替としてもよく使われます。

const HttpStatus = {
  OK: 200,
  NotFound: 404,
  ServerError: 500,
} as const;

type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus];
// type HttpStatus = 200 | 404 | 500

function logStatus(code: HttpStatus): void {
  console.log(`HTTPステータス: ${code}`);
}

logStatus(HttpStatus.OK); // OK

enum はコンパイル後のJavaScriptに余分なコードを生成することがありますが、as const オブジェクトは通常のオブジェクトなので、余計なコードを増やさずに同じような使い勝手を実現できます。

まとめ

この記事のポイント

  • オブジェクトや配列のプロパティ値は、デフォルトでは stringnumber のように広く推論される
  • as const をつけると、値がリテラル型として固定され、readonly にもなる
  • typeof 配列 as const [number] で、配列の中身からunion型を自動生成できる
  • 定数として決め打ちで扱う文字列・数値は、as const をつけておくとunion型への受け渡しでエラーになりにくい
  • satisfies と組み合わせると、値のリテラル型を保ったまま型の形もチェックできる

次に読むべき記事

型を固定する as const を理解したら、次はnullundefinedを扱う際に登場する非nullアサーションを見ていきましょう。

→ 次の記事:非nullアサーション(!)を使うべき場面・避けるべき場面

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