TypeScriptにおいて、型の安全性を最大限に高めるためには、コンパイラがどのように型を推論するかを正確に制御することが不可欠です。

特に、定数として定義した値が意図せず広い型として推論されてしまう「型の拡大(Literal Widening)」は、大規模な開発において予期せぬバグの原因となることがあります。

このような課題を解決し、値をリテラル型として厳格に扱うための機能が「as const(constアサーション)」です。

本記事では、as constの基本的な仕組みから、オブジェクトや配列への適用、さらには実務で役立つ応用テクニックまでを詳しく解説します。

TypeScriptにおけるas const(constアサーション)の基本

TypeScriptのas constは、式に対して「リテラル型」としての性質を付与する特殊な構文です。

通常、TypeScriptで変数を定義すると、その値は変更される可能性があると判断され、より広い型に推論されます。

例えば、文字列の “admin” を変数に代入した場合、デフォルトでは string 型として扱われることが一般的です。

しかし、as constを使用することで、その値そのものを型として固定し、変更を禁止することができます。

この機能はTypeScript 3.4から導入され、定数定義の柔軟性と安全性を劇的に向上させました。

constアサーションを記述する際は、値の末尾に as const と記述するか、あるいは <const> という構文を使用します。

現代のTypeScript開発においては、可読性の観点から as const という記述方法が広く推奨されています。

型の拡大(Literal Widening)とは何か

as constの意義を理解するためには、まず「型の拡大(Literal Widening)」という挙動を知る必要があります。

TypeScriptでは、let を用いて変数を宣言すると、将来的に別の値が代入されることを想定して型が推論されます。

以下のコード例で、その挙動を確認してみましょう。

TypeScript
// letで宣言した場合
let userRole = "admin"; 

// 途中で別の文字列を代入可能
userRole = "editor";

この場合、userRole の型は “admin” 型ではなく、より汎用的な string 型として推論されています。

一方で、const を使用して変数を宣言した場合はどうでしょうか。

TypeScript
// constで宣言した場合
const defaultRole = "admin";

const で宣言されたプリミティブ値は、再代入が不可能なため、TypeScriptはこれを “admin” というリテラル型として推論します。

しかし、オブジェクトや配列の場合は、const で宣言しても内部のプロパティを書き換えることが可能です。

そのため、オブジェクトのプロパティなどはデフォルトで型の拡大が発生してしまいます。

この「オブジェクト内部の型まで厳格に固定したい」というケースで、as constが非常に重要な役割を果たします。

as constの主な効果と挙動

as constを適用すると、TypeScriptコンパイラに対して以下の3つの強力な制約を指示することになります。

  • リテラル型が拡大されない(例: “hello” が string ではなく “hello” 型になる)。
  • オブジェクトのプロパティがすべて readonly (読み取り専用)になる。
  • 配列が readonly な「タプル型」として扱われる。

これらの挙動を、具体的なコードを用いて詳しく見ていきましょう。

配列を読み取り専用のタプルにする

通常の配列定義では、要素の追加や削除が自由に行えるため、型は string[]number[] のようになります。

しかし、as constを付与すると、その配列は中身が固定されたタプル型へと変化します。

TypeScript
const directions = ["north", "south", "east", "west"] as const;

// 型推論の結果:readonly ["north", "south", "east", "west"]
// directions.push("up"); // エラー:読み取り専用のため変更不可

このように、配列の要素数と各要素の値を完全に固定できるのが大きなメリットです。

オブジェクトのプロパティを再帰的にreadonlyにする

オブジェクトに対して as const を使用すると、ネストされた深い階層のプロパティまで含めて readonly が付与されます。

TypeScript
const config = {
    apiEndpoint: "https://api.example.com",
    retryCount: 3,
    options: {
        debug: true
    }
} as const;

// config.apiEndpoint = "http://localhost"; // エラー
// config.options.debug = false; // エラー(深い階層もreadonly)

通常、Object.freeze() を使用しても、JavaScriptのランタイムではネストされたオブジェクトまで凍結するには再帰的な処理が必要です。

TypeScriptの as const を使えば、コンパイルレベルですべての階層のプロパティを定数化することができます。

具体的な活用シーン

as constは単に値を固定するだけでなく、他のTypeScriptの機能と組み合わせることで真価を発揮します。

実務でよく使われるパターンをいくつか紹介します。

ユニオン型を配列から動的に生成する

これが最も頻繁に使われるテクニックの一つです。

「ある特定の文字列のリスト」があり、それを配列としても使いたいし、型としても定義したい場合に便利です。

TypeScript
const PLATFORMS = ["iOS", "Android", "Web"] as const;

// 配列からユニオン型( "iOS" | "Android" | "Web" )を抽出
type Platform = typeof PLATFORMS[number];

function getPlatformIcon(platform: Platform) {
    // 引数platformは "iOS", "Android", "Web" のいずれかのみ受け付ける
    console.log(platform);
}

もし as const がなければ、typeof PLATFORMS[number] は単なる string 型になってしまいます。

この手法を使えば、マスタデータの定義を一箇所にまとめつつ、型の安全性も確保できます。

オブジェクトのキーから型を抽出する

設定オブジェクトのキーを型として利用したい場合も、as constが必須となります。

TypeScript
const THEME_COLORS = {
    primary: "#007bff",
    secondary: "#6c757d",
    success: "#28a745"
} as const;

type ThemeColorKey = keyof typeof THEME_COLORS; // "primary" | "secondary" | "success"
type ThemeColorValue = typeof THEME_COLORS[ThemeColorKey]; // "#007bff" | "#6c757d" | "#28a745"

このように、「値」を正解のソース(Source of Truth)として扱い、そこから型を自動生成するアプローチが可能になります。

Discriminated Unions(判別可能なユニオン型)の補助

Reduxのアクションや、APIレスポンスの型定義などで、特定のプロパティを使って型を絞り込む際にも有効です。

TypeScript
const createAction = (type: string, payload: any) => ({ type, payload } as const);

const action = createAction("SET_USER", { name: "Tanaka" });
// action.type の型は string ではなく "SET_USER" に固定される

型を string ではなくリテラル型として保持することで、switch 文や if 文による型ガードが正確に機能するようになります。

as const と Object.freeze() の違い

よくある疑問として、「JavaScript標準の Object.freeze() と何が違うのか」というものがあります。

以下の表で、主な違いを比較してみましょう。

比較項目as constObject.freeze()
実行されるタイミングコンパイル時(TypeScript)実行時(JavaScript)
ネストされた要素再帰的に適用される直下の階層のみ(浅い凍結)
型推論リテラル型に固定される基本は元の型のまま
ランタイムへの影響なし(JS変換後は消える)あり(値を書き換えるとエラーまたは無視)

実務においては、型安全性を高める目的ならば as const を使用し、実行時の意図しない変更を確実に防ぐ必要があるなら Object.freeze() を併用するのがベストプラクティスです。

as const を使用する際の注意点

非常に便利な as const ですが、使用するにあたって理解しておくべき制約も存在します。

読み取り専用(readonly)になることの副作用

as const を適用した値は、すべて readonly になります。

そのため、その値を readonly ではない通常の型を期待している関数に渡すと、型エラーが発生することがあります。

TypeScript
function updateTags(tags: string[]) {
    tags.push("new-tag");
}

const myTags = ["it", "tech"] as const;
// updateTags(myTags); // エラー:readonly string[] を string[] に渡すことはできない

このような場合は、受け取り側の関数でも ReadonlyArray を使用するか、スプレッド構文を使ってコピーを作成する必要があります。

TypeScript
// 解決策1: スプレッド構文でコピーする
updateTags([...myTags]);

// 解決策2: 関数側で読み取り専用を受け入れる
function logTags(tags: readonly string[]) {
    console.log(tags);
}
logTags(myTags); // OK

動的な値には使用できない

as const はあくまでリテラルに対して適用するものです。

実行時に決まる変数の値や、計算結果に対して as const を付与しても、期待通りのリテラル型推論は得られません。

TypeScript
let category = "news";
const status = category as const; // これは string 型のまま(リテラル化されない)

あくまでコード上に直接書かれたリテラル値に対して使用することを意識してください。

パフォーマンスと保守性への影響

as const を多用することで、コンパイル速度に影響が出るのではないかという懸念を持つ方もいるかもしれません。

結論から言えば、現代のTypeScriptコンパイラにおいて、通常の定数定義で as const を使用することによるパフォーマンスの低下は無視できるレベルです。

むしろ、型が明確になることでコンパイラが余計な型推論(拡大)を行う必要がなくなり、コードの意図が明確化されるメリットの方が大きいです。

保守性の観点からも、マジックナンバーやマジックストリングを排除し、それらをリテラル型のユニオンとして扱う手法は非常に強力です。

型定義の「二重管理(配列の定義と型の定義を別々に行うこと)」を防げるため、開発効率の向上にも直結します。

まとめ

TypeScriptの as const(constアサーション)は、型推論の挙動をきめ細やかに制御するための非常に重要な機能です。

「型の拡大」を防ぎ、リテラル型としての厳密さを維持することで、実行前のエラー検知能力を飛躍的に高めることができます。

特に配列やオブジェクトからユニオン型を生成するテクニックは、現代のTypeScript開発における必須スキルと言っても過言ではありません。

readonly という制約が加わる点には注意が必要ですが、それを逆手に取ることで、より堅牢で予測可能なコードベースを構築できるでしょう。

まずは、プロジェクト内の定数管理や、設定オブジェクトの定義から as const を活用してみてください。

コードの意図が型として明確に表現される快感を、ぜひ実感していただければと思います。