TypeScript で型安全性を高めるとき、as const は強力な武器になる。しかし、どこにでも付ければよいわけではない。リテラル型推論の仕組みを理解し、適切な場所で使うことが重要だ。
リテラル型推論とは何か
TypeScript は変数の宣言方法によって、型の推論結果を変える。
const x = "hello"; // 型は "hello" let y = "hello"; // 型は string
const で宣言した変数は再代入できないため、TypeScript は値そのものをリテラル型として推論する。一方 let は再代入の可能性があるので、より広い型(この場合は string)に拡大される。これをリテラル型の widening(型の拡大)と呼ぶ。
問題はオブジェクトや配列だ。const で宣言しても、プロパティや要素は変更できるため、widening が起きてしまう。
const config = { endpoint: "/api/users", method: "GET" }; // 型は { endpoint: string; method: string }
ここで as const の出番となる。
as const が効果を発揮する場面
as const を付けると、オブジェクト全体が readonly になり、すべてのプロパティがリテラル型として推論される。
const config = { endpoint: "/api/users", method: "GET" } as const; // 型は { readonly endpoint: "/api/users"; readonly method: "GET" }
これにより config.method は string ではなく "GET" というリテラル型になる。HTTP メソッドを "GET" | "POST" | "PUT" | "DELETE" のようなユニオン型で受け取る関数に渡すとき、型エラーを防げる。
配列でも同様の効果がある。
const colors = ["red", "green", "blue"] as const; // 型は readonly ["red", "green", "blue"]
as const なしでは string[] と推論されるが、付けることでタプル型になり、各要素がリテラル型として保持される。
付けるべき典型的なパターン
設定オブジェクトや定数テーブルを定義するときは、as const が有効だ。
const STATUS_CODES = { OK: 200, NOT_FOUND: 404, INTERNAL_ERROR: 500 } as const; type StatusCode = typeof STATUS_CODES[keyof typeof STATUS_CODES]; // 型は 200 | 404 | 500
as const がなければ StatusCode は number になってしまい、型安全性が失われる。
列挙的な配列から型を生成する場合も同様だ。
const DIRECTIONS = ["north", "south", "east", "west"] as const; type Direction = typeof DIRECTIONS[number]; // 型は "north" | "south" | "east" | "west"
関数の引数にリテラル型を渡したいときも as const が役立つ。
function request(method: "GET" | "POST", url: string) { /* ... */ } const options = { method: "GET", url: "/api" } as const; request(options.method, options.url); // OK
as const がなければ options.method は string 型となり、型エラーが発生する。
付けてはいけない場面
as const は万能ではない。むしろ付けることで問題が生じるケースもある。
可変であるべきデータに付けてしまうと、実行時エラーの原因になる。TypeScript は readonly を型レベルでのみチェックし、実行時には何も起きない。しかし、意図せず readonly な型を可変な型を期待する関数に渡すと、型エラーになる。
const items = [1, 2, 3] as const; items.push(4); // 型エラー: Property 'push' does not exist on type 'readonly [1, 2, 3]'
API レスポンスなど、外部から来るデータに as const を付けるのも避けるべきだ。
// やってはいけない const response = await fetch("/api/user").then(r => r.json()) as const;
実行時の値は何でもありえるのに、型だけが固定されてしまう。これは型の嘘であり、バグの温床になる。外部データには適切な型定義やバリデーションを使うべきだ。
ジェネリック関数の内部で as const を使うと、期待どおりに動かないことがある。
function wrap<T>(value: T) { return { value } as const; } const result = wrap("hello"); // 型は { readonly value: "hello" } ではなく { readonly value: string }
T はすでに string に推論された後なので、as const を付けても手遅れだ。リテラル型を保持したいなら、呼び出し側で as const を使うか、ジェネリクスの制約を工夫する必要がある。
function wrap<const T>(value: T) { return { value }; } const result = wrap("hello"); // 型は { value: "hello" }
TypeScript 5.0 以降では const 型パラメータが使える。これにより、呼び出し側で as const を付けなくてもリテラル型が推論される。
型アサーションとの違いを意識する
as const は型アサーションの一種だが、通常の型アサーション(as SomeType)とは性質が異なる。
開発者が「この値はこの型だ」と TypeScript に伝える。型チェックを部分的にバイパスするため、誤った使い方をすると実行時エラーにつながる。
値の構造から最も狭い型を推論させる。型を嘘つくのではなく、推論の精度を上げる指示だ。値と型の整合性は保たれる。
as const は安全な型アサーションだが、それでも使いどころを誤ると問題が起きる。readonly であるべきでないものに付けない、外部データに付けない、この 2 点を守れば大きな事故は防げる。