【入門編】TypeScriptのWideningを意図的に抑制する:as constと型注釈の使い分け – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptを触り始めると、なんだか変数が勝手に広い型に化けてしまって、コンパイラに怒られたり、補完がイマイチ効かなくてモヤモヤした経験はありませんか?

「文字列を入れたはずなのに、`string` 型になってしまって特定の値に固定できない……」
「オブジェクトのプロパティを定数として使いたいのに、書き換え可能な型にされてしまう……」

これらは、TypeScriptの「Widening(型の拡幅)」という挙動が裏で働いているのが原因なんですよね。
ここをしっかりと理解してコントロールできるようになると、TypeScriptの基本はバッチリマスターできたと言って過言ではありません!

今回は、このWideningを意図的に抑制するための最強の武器である `as const` と「型注釈」の使い分けについて、裏側の型評価の仕組みも含めて優しく紐解いていきましょう。

—

1. なぜ勝手に型が広がってしまうのか?(Wideningの正体)

まずは、TypeScriptがコードを書いたときに裏でどんな風に型を推論しているのかを見てみましょう。

日常的に書くこのようなコードを思い浮かべてみてください。

let mode = “development”;

私たち人間の脳内では、「この `mode` は `”development”` という文字列のまま固定でしょ!」と思いますよね。しかし、TypeScriptのコンパイラはこう評価します。

// コンパイラが見ている世界
let mode: string = “development”;

あれっ、`”development”` というピンポイントなリテラル型だったはずが、いつの間にか `string` という広い海(プリミティブ型)に広げられて(Widened)しまいました。これが Widening(型の拡幅) です。

なぜこんなお節介なことをするのでしょうか? それは、`let` で宣言された変数は「後から別の文字列を再代入するかもしれない」とTypeScriptが予測するからです。もし `let mode = “development”;` の型が `”development”` 固定のままだったら、後から `mode = “production”;` と代入したときに「型が違う!」とエラーになってしまいますよね。それを防ぐために、あらかじめ広めの型に推論してくれているわけです。

—

2. Wideningを止める2つのアプローチ:型注釈 vs `as const`

「いやいや、私はこの値を絶対に書き換えないし、この特定の値(リテラル型)として扱いたいんだ!」という場面は、フロントエンド開発(特に設定値やステート管理、ルーティング定義など)において本当によく出てきます。

ここで登場するのが、「型注釈」 と `as const`(constアサーション) です。
この2つは似ているようで、コンパイラに対する指示の意味が全く異なります。それぞれの動きを詳しく見ていきましょう。

アプローチA:型注釈(Type Annotation)で「枠」を狭める

変数の後ろに `: 期待する型` を書くのが型注釈です。

// 型注釈を使って、あらかじめ「この型として扱う」と宣言する
const currentMode: “development” | “production” | “test” = “development”;

  • どう動くか: 変数宣言時に「この型以外は入れさせない」という厳格な枠(型)を明示します。
  • メリット: 変数自体は `const` でなくても `let` で宣言して後から別の値(同じユニオン型の範囲内)を代入できるようになります。
  • 向いているケース: 状態管理などで、決まった選択肢のいずれかの値を保持しつつ、将来的に値を再代入したいとき。

アプローチB:`as const`(Const Assertion)で「凍結」する

値のすぐ後ろに `as const` を書くのが、TypeScript 3.4から導入された強力な機能「Const Assertion」です。

// as const を使って、値も型も完全にロックダウンする
const config = {
endpoint: “https://api.example.com”,
timeout: 5000,
methods: [“GET”, “POST”]
} as const;

これをコンパイラがどう評価するか、その型をのぞいてみましょう。

// コンパイラが推論する型
// (すべてのプロパティが readonly になり、リテラル型に固定される)
const config: {
readonly endpoint: “https://api.example.com”;
readonly timeout: 5000;
readonly methods: readonly [“GET”, “POST”];
}

すごいですね! `as const` をつけると、以下の変化が同時に起きます。
1. リテラル型の維持: `”https://api.example.com”` や `5000` といった具体的な値の型がそのまま維持され、`string` や `number` に広がりません。
2. 配列のタプル化: 配列は普通の `string[]` ではなく、読み取り専用のタプル型(`readonly [“GET”, “POST”]`)になります。
3. 完全なイミュータブル化: すべてのプロパティに自動的に `readonly` が付与され、うっかりプロパティを書き換えようものなら、コンパイルエラーでガッチリガードしてくれます。

—

3. 実践! どっちをどう使い分けるべき?

「じゃあ、全部 `as const` にしておけば万事解決なんじゃ……?」と思いがちですが、適材適所があります。開発現場での具体的な使い分けの基準を整理しておきましょう。

ケース1:APIのレスポンス型や設定オブジェクト、定数マップ(`as const` の独壇場)

画面に表示する固定のラベル一覧や、ステータスのマッピング定義などは、`as const` の最高の活躍場所です。

// ステータス定義の例
const TaskStatus = {
TODO: “todo”,
IN_PROGRESS: “in_progress”,
DONE: “done”,
} as const;

// これにより、TaskStatus.TODO は “todo” 型になり、
// オブジェクトのプロパティの書き換えもコンパイル時に禁止されます。

// さらに、型を逆引きしたいときにも as const が火を吹きます!
type TaskStatusType = typeof TaskStatus[keyof typeof TaskStatus];
// 成果物型: “todo” | “in_progress” | “done”

この `typeof TaskStatus[keyof typeof TaskStatus]` というイディオムは、実務で本当によく使います。「オブジェクトの値からユニオン型を自動生成する」ための黄金パターンですので、ぜひ覚えておいてくださいね。

ケース2:関数の引数や、フォームの入力値など「可変だが制限したい」とき(型注釈)

一方で、値は動的に変わる可能性があるけれど、受け付ける値のパターンを制限したい場合は「型注釈」の出番です。

type Environment = “development” | “staging” | “production”;

// 再代入可能な変数として持ちたい場合は、型注釈を使う
let currentEnv: Environment = “development”;

// 後から別の正しい環境に切り替えるのはOK
currentEnv = “production”;

// ❌ 別の文字列を入れると、コンパイラが即座にエラーを出して守ってくれる
// currentEnv = “local”; // Error: Type ‘”local”‘ is not assignable to type ‘Environment’.

—

まとめ:型システムを手なづけよう

今回は、TypeScriptのWidening(型の拡幅)の仕組みと、それを抑制する「型注釈」と「`as const`」の使い分けについて解説しました。

  • Widening: デフォルトでは `let` などの変数は広めの型(`string` や `number` など)に自動で推論される。
  • 型注釈: 変数に「枠」をはめて、特定の型(ユニオン型など)に制限したいときに使う(再代入可能)。
  • `as const`: 値と構造をその場で完全に固定(イミュータブル化)し、リテラル型を維持したいときに使う。

これら二つのアプローチを自在に使い分けられるようになると、TypeScriptのコンパイラがあなたにとって「邪魔な監視者」ではなく、「頼れる最高の相棒」に変わるはずです。

ここをクリアできれば、もう基本の型システムはバッチリです!自信を持って次のステップへ進んでいきましょう!

タイトルとURLをコピーしました