as const を付けるべき場所、付けてはいけない場所

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.methodstring ではなく "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 がなければ StatusCodenumber になってしまい、型安全性が失われる。

列挙的な配列から型を生成する場合も同様だ。

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.methodstring 型となり、型エラーが発生する。

付けてはいけない場面

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 点を守れば大きな事故は防げる。