できない.dev

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. 要素単位で付けている: [1 as const, 2 as const][1, 2] ではなく (1 | 2)[] のままで、タプル化が起きない。
    配列全体に付けないとタプルにならない。
  2. 戻り値で widening: function foo() { return [42, 'ok']; }(string | number)[] に widened される。
    タプル [number, string] にしたいなら return [42, 'ok'] as const
  3. 型注釈での上書き: const x: string[] = ['a', 'b'] as const は注釈側が優先され、リテラル情報が消える。
    注釈を外すか as const のみにする。
  4. 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 が効く。

この記事は役立ちましたか?