as const を付けるべき場所、付けてはいけない場所
TypeScript で型安全性を高めるとき、as const は強力な武器になる。しかし、どこにでも付ければよいわけではない。リテラル型推論の仕組みを理解し、適切な場所で使うことが重要だ。
リテラル型推論とは何か
TypeScript は変数の宣言方法によって、型の推論結果を変える。
const x = "hello"; // 型は "hello"
let y = "hello"; // 型は stringconst で宣言した変数は再代入できないため、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 | 500as 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); // OKas 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値の構造から最も狭い型を推論させる。型を嘘つくのではなく、推論の精度を上げる指示だ。値と型の整合性は保たれる。
as const は安全な型アサーションだが、それでも使いどころを誤ると問題が起きる。readonly であるべきでないものに付けない、外部データに付けない、この 2 点を守れば大きな事故は防げる。