TypeScript の as const で型が絞り込めない
`as const` はリテラル型と readonly 化を行うが、配置場所を間違えると効かない。
配列やタプル全体に付ける必要があり、関数の戻り値で union 化を保ちたいなら戻り値式に付ける。
要素単位の as const では狙った narrowing が起きない。
#typescript#as-const#narrowing#literal-type#tuple
公開:
要約
as const は値式に対して 3 つの効果を持つ: リテラル型の widening を止める、配列をタプルにする、プロパティを readonly にする。
これらは「式単位」で効くため、配置場所を 1 つズラすだけで意図と違う型になる。
union narrowing を狙うときは「外側全体」に付け、関数の戻り値でリテラルを保ちたいなら return 式に付ける。
よくある原因
- 要素単位で付けている:
[1 as const, 2 as const]は[1, 2]ではなく(1 | 2)[]のままで、タプル化が起きない。
配列全体に付けないとタプルにならない。 - 戻り値で widening:
function foo() { return [42, 'ok']; }は(string | number)[]に widened される。
タプル[number, string]にしたいならreturn [42, 'ok'] as const。 - 型注釈での上書き:
const x: string[] = ['a', 'b'] as constは注釈側が優先され、リテラル情報が消える。
注釈を外すかas constのみにする。 - satisfies の不在: 型チェックは欲しいが widening は避けたいケースで
satisfiesを使わないと、注釈の型に合わせて広がる。
解決策
1. 配列全体に as const
const routes = ["home", "about", "contact"] as const;
// type Routes = "home" | "about" | "contact"
type Routes = (typeof routes)[number];const assertions の導入記事 に挙動の網羅例がある。
2. 戻り値式に as const
function parse(input: string) {
if (input === "") return ["err", "empty"] as const;
return ["ok", input] as const;
}
const r = parse("hi");
if (r[0] === "ok") {
// r[1]: string
}タプル化と union 判別が同時に効き、r[0] での判別だけで r[1] の型も絞られる。
3. satisfies で型チェックと両立
const colors = {
primary: "#0070f3",
danger: "#ef4444",
} as const satisfies Record<string, `#${string}`>;satisfies を後ろに置くと、型チェックは効くが推論型はリテラルのまま残る。as const だけ → as const satisfies T が定石。
4. discriminated union の作成
type Event =
| { kind: "click"; x: number; y: number }
| { kind: "key"; code: string };
const evt = { kind: "click", x: 1, y: 2 } as const satisfies Event;kind 側もリテラル化されるため、関数で Event を受けたときに kind === "click" で narrowing が効く。