1. ホーム
  2. TypeScript

【TypeScript】as const の使い方|値をリテラル型・読み取り専用(readonly)にする

Share

TypeScript で文字列や数値のリテラルを変数に入れると、多くの場合はより広い型(stringnumber)に推論されます。しかし「この値はこの文字列のまま」「この配列は書き換えない」といった、値そのものを型として固定したい場面があります。そこで使うのが as const(const アサーション)です。この記事では、as const を付けたときと付けないときの推論の違い、オブジェクトや配列が読み取り専用かつリテラル型で固定される仕組み、設定値からユニオン型を取り出す方法や配列をタプルとして扱う実践例まで、初心者〜中級者向けに解説します。

as const を付けないときの型の広がり

まず、as const がない状態で TypeScript がどう型を推論するかを確認します。リテラルの値を letconst に入れたとき、変数の宣言方法によって推論される型が変わります。

widen.ts
// let は再代入できるので、広い型(string)に推論される
let mode1 = "dark"; // 型: string

// const(プリミティブ)は再代入できないので、リテラル型に推論される
const mode2 = "dark"; // 型: "dark"

// ただしオブジェクトのプロパティは const でも広がる
const config = {
  mode: "dark", // 型: string("dark" ではない)
};
config.mode = "light"; // 後で書き換えられるため string と推論される

const mode2 = "dark" のようにプリミティブ値を const に入れた場合は、再代入できないため "dark" というリテラル型に固定されます。しかしオブジェクトの中身は、const で宣言していてもプロパティごとに書き換えが可能なため、config.mode"dark" ではなく string に広がってしまいます。この「オブジェクトや配列の中身が広い型になる」挙動を抑えたいときに as const が役立ちます。

as const とは何か

as const(const アサーション)は、値の後ろに as const と書くことで、その値を「可能な限り狭いリテラル型」かつ「読み取り専用(readonly)」として扱うようコンパイラに指示する構文です。オブジェクトや配列に付けると、内側のプロパティや要素まで再帰的に固定されます。先ほどのオブジェクトに as const を付けて、推論がどう変わるかを見てみます。

as-const-basic.ts
const config = {
  mode: "dark",
  retryCount: 3,
} as const;
// 推論される型:
// {
//   readonly mode: "dark";
//   readonly retryCount: 3;
// }

config.mode;       // 型: "dark"(string ではない)
config.retryCount; // 型: 3(number ではない)

as const を付けると、config.mode"dark"config.retryCount3 というリテラル型に固定され、さらに各プロパティが readonly になります。値を「その値のまま」型として扱いたいときの基本形です。

付けたときと付けないときの違いを整理する

as const の有無で推論される型がどう変わるかを、代表的なパターンで比較します。ポイントは「型がリテラルに固定されるか」と「読み取り専用になるか」の2点です。

書き方as const なしas const あり
const s = "on""on"(const なので元々リテラル)"on"
{ k: "on" }{ k: string }{ readonly k: "on" }
[1, 2, 3]number[]readonly [1, 2, 3](タプル)
書き換えできるできない(readonly

特に配列の違いは大きく、as const なしでは要素数が決まっていない number[] になりますが、as const を付けると要素の値と順序、長さまで固定された readonly [1, 2, 3] というタプル型になります。この違いが、後述する型安全な配列操作につながります。

設定値からユニオン型を取り出す

as const の代表的な使いどころが、定数オブジェクトから型を導出するパターンです。値をリテラル型で固定できるため、typeofkeyof を組み合わせて「そのオブジェクトが取り得る値」をユニオン型として取り出せます。

roles.ts
const ROLE = {
  Admin: "admin",
  Editor: "editor",
  Viewer: "viewer",
} as const;

// 値のユニオン型を取り出す
type Role = typeof ROLE[keyof typeof ROLE];
// 型: "admin" | "editor" | "viewer"

function setRole(role: Role) {
  console.log(role);
}

setRole(ROLE.Admin); // OK
setRole("editor");   // OK
setRole("guest");    // エラー:Role に存在しない値

typeof ROLE でオブジェクトの型を取得し、keyof typeof ROLE"Admin" | "Editor" | "Viewer")でキーのユニオンを得て、それを添字に使うことで値のユニオン "admin" | "editor" | "viewer" が得られます。as const がなければ各値が string に広がってしまい、Role はただの string になって、この絞り込みはできません。定数と型を一箇所で管理でき、値を追加すれば型も自動で追従するのが利点です。

配列を readonly タプルにして型安全に扱う

配列に as const を付けると読み取り専用のタプルになります。要素の値がリテラル型で固定されるため、map などで加工しても各要素の型情報が保たれ、そこからユニオン型を作ることもできます。

fruits.ts
const FRUITS = ["apple", "banana", "orange"] as const;
// 型: readonly ["apple", "banana", "orange"]

// 要素のユニオン型を取り出せる
type Fruit = typeof FRUITS[number];
// 型: "apple" | "banana" | "orange"

// map で加工しても要素の型は保たれる
const labels = FRUITS.map((f) => f.toUpperCase());
// f は "apple" | "banana" | "orange" と分かるので安全に扱える

function pick(fruit: Fruit) {
  return fruit;
}
pick("apple");  // OK
pick("grape");  // エラー:Fruit に存在しない値

typeof FRUITS[number] は「タプルのすべての要素の型」を意味し、ここでは "apple" | "banana" | "orange" になります。as const がなければ FRUITSstring[] となり、typeof FRUITS[number] はただの string になってしまいます。定義した配列の値をそのまま型として使い回せるのが、as const を配列に付ける大きなメリットです。

判別可能なユニオンのアクション定数に使う

状態管理などで使う「アクション」を表すオブジェクトにも as const が向いています。type プロパティをリテラル型で固定できるため、判別可能なユニオン(discriminated union)として switch で安全に分岐できます。

action.ts
function increment() {
  return { type: "increment", amount: 1 } as const;
}
// 戻り値の型: { readonly type: "increment"; readonly amount: 1 }
// as const がないと type は string になる

function reset() {
  return { type: "reset" } as const;
}

type Action = ReturnType<typeof increment> | ReturnType<typeof reset>;

function reducer(count: number, action: Action): number {
  switch (action.type) {
    case "increment":
      return count + action.amount; // amount は number として扱える
    case "reset":
      return 0;
  }
}

as const によって type"increment""reset" というリテラル型に固定されるため、switch (action.type) の各 case で TypeScript がアクションの種類を正しく絞り込めます。as const がないと typestring になり、判別可能なユニオンとして機能しません。

as const で変わるのは型だけという点に注意する

as const は型レベルの指定であり、コンパイル後の JavaScript には何も残りません。つまり実行時の値そのものは一切変わらず、あくまでコンパイラ上での型の扱いが変わるだけです。この前提を押さえておかないと、期待とずれた挙動に戸惑うことがあります。

readonly は型チェック上の制約である

as const を付けたオブジェクトや配列は readonly になり、プロパティへの再代入や配列の push などがコンパイルエラーになります。ただしこれは型チェック上の制約であって、JavaScript としてのオブジェクトが凍結されるわけではありません。実行時に本当に変更を防ぎたい場合は、別途 Object.freeze() を使う必要があります。

readonly-error.ts
const point = { x: 0, y: 0 } as const;
point.x = 10; // エラー:読み取り専用プロパティには代入できない

const nums = [1, 2, 3] as const;
nums.push(4);  // エラー:readonly な配列に push は無い
nums[0] = 100; // エラー:読み取り専用のインデックスには代入できない

このように、as const を付けた値を書き換えようとすると型エラーになります。定数として扱いたい値には有効ですが、後から中身を更新したいデータには向きません。用途に合わせて使い分けてください。

まとめ

as const(const アサーション)は、値を「可能な限り狭いリテラル型」かつ「読み取り専用」として扱うための構文です。オブジェクトや配列に付けると、内側のプロパティや要素まで再帰的にリテラル型で固定され、readonly になります。これにより、定数オブジェクトから typeof obj[keyof typeof obj] で値のユニオン型を取り出したり、配列をタプルにして map で型安全に扱ったり、判別可能なユニオンのアクション定数を作ったりできます。注意点として、as const が変えるのは型だけで実行時の値は変わらず、readonly も型チェック上の制約にとどまります。値をそのまま型として固定したい場面で活用してみてください。

参考ページ