こんにちは!日々の開発、お疲れ様です。
TypeScriptを書き始めた頃、「あれ?さっき定義したはずの値が、なぜか普通の `string` 型になってエラーになるぞ……?」と首を傾げた経験はありませんか?
今回は、TypeScriptの型システムが裏側でコソコソと行っている「Widening(型広げ)」というお節介な機能と、それを華麗にコントロールする `as const` や型注釈のテクニックについて、一緒に深く掘り下げていきましょう。
ここをクリアすれば、TypeScriptの挙動が手に取るようにわかるようになりますよ。バッチリマスターしていきましょうね!
—
1. 変数の型が勝手に広がる?「Widening(型広げ)」の正体
TypeScriptの最大の魅力は、JavaScriptのコードに緻密な型を与えることで、実行時エラーをコンパイル時に根絶やしにできることです。
例えば、以下のようなコードを考えてみましょう。
let direction = “left”;
この時、変数 `direction` の型は何だと思いますか?
「”left” という文字列リテラル型でしょ?」と思われた方、惜しい!実はこれ、TypeScriptは以下のように解釈しています。
let direction: string = “left”;
なんと、具体的な `”left”` という値の型が、親玉である広範な `string` 型に格上げ(=Widening)されてしまいました。
なぜTypeScriptはこんなお節介をするのか?
これはTypeScriptの設計思想に由来します。`let` で宣言された変数は、「後から値が書き換えられる(再代入される)」可能性があるためです。
let direction = “left”; // 型は string
direction = “right”; // string だから再代入しても怒られない!
direction = “up”; // これもOK
「後から別の文字列が入るかもしれないから、厳密な `”left”` よりも、安全のために `string` という広い型にしておこう」という、コンパイラなりの優しい配慮なんです。
しかし、私たち開発者からすると、「いやいや、この変数は一生 `”left”` のまま使いたいんだよ!」という場面も多々ありますよね。このお節介をピタッと止める方法が、これから紹介する `as const` と 明示的な型注釈 です。
—
2. 意図しない型広げを防ぐ2つのアプローチ
Wideningを抑制し、特定のリテラル型を維持したいときは、主に以下の2つの手段を用います。状況によって使い分けるのがプロの技ですよ。
アプローチA:`as const`(constアサーション)で鉄壁のイミュータブルを作る
もっとも強力なのが、値の直後に `as const` を添える方法です。これはコンパイラに対して、「この値、そしてこれに含まれるすべてのプロパティを、変更不可(イミュータブル)な極限のリテラル型として固定しなさい」と命令する呪文です。
// as const を使った場合
const config = {
endpoint: “https://api.example.com”,
timeout: 5000,
} as const;
/
【configの型評価】
{
readonly endpoint: “https://api.example.com”;
readonly timeout: 5000;
}
/
`as const` を使うと、以下の2つの素晴らしい変化が起きます。
1. 型が狭いまま固定される(Wideningが起きない):`string` や `number` にならず、具体的なリテラル型を維持します。
2. すべてが `readonly` になる:うっかりプロパティを書き換えるバグを防げます。
アプローチB:明示的な型注釈で「枠」を先に決める
もう一つの方法は、変数宣言時に `:`(コロン)を使って、あらかじめ許容する型を私たちが直接指定してあげる方法です。
// 型注釈を使った場合
const role: “admin” = “admin”;
これなら、コンパイラは「最初から `admin` しか入らない枠なんだな」と理解するので、Wideningは発生しません。
—
3. 実践!よくある罠と回避テクニック
それでは、実際の開発現場でやりがちな「Wideningの罠」と、そのスマートな解決策を見てみましょう。
罠:オブジェクトのプロパティで型が広がり、関数の引数で弾かれる
APIのリクエストパラメータなどを定義しているときに、この罠によくハマります。
// ❌ 陥りがちなコード
const requestOptions = {
method: “POST”,
retries: 3,
};
// 関数側は “POST” という特定のリテラル型を求めているとする
function sendRequest(method: “GET” | “POST”, retries: number) {
// …
}
// ここでコンパイルエラー!
// 理由: requestOptions.method は string 型に広げられているため、
// “GET” | “POST” の枠に入らないと判定される。
sendRequest(requestOptions.method, requestOptions.retries);
【エラーの脳内トレース】
コンパイラ視点では、`requestOptions.method` は「あとから書き換えられるかもしれないから `string` 型だよね。一方、`sendRequest` は `”GET”` か `”POST”` しか受け付けないよ。`string` をそのまま渡すのは危険だからコンパイルエラーにするね!」と判断しています。
解決策1:`as const` で全体を固める
オブジェクト全体を定数として扱いたい場合は、末尾に `as const` を付けるのが最もスマートです。
// ✅ 解決策1: as const を活用する
const requestOptions = {
method: “POST”,
retries: 3,
} as const;
// requestOptions.method は “POST” 型なので、無事に渡せる!
sendRequest(requestOptions.method, requestOptions.retries);
解決策2:型注釈で変数の器をあらかじめ限定する
「オブジェクトのプロパティは後から変えたいけれど、特定の型枠にはめておきたい」という場合は、型注釈を使います。
// ✅ 解決策2: 型注釈を使う
const requestOptions: {
method: “GET” | “POST”;
retries: number;
} = {
method: “POST”,
retries: 3,
};
sendRequest(requestOptions.method, requestOptions.retries); // バッチリOK!
—
まとめ:使い分けの指針
ここまでの内容を、すっきりと整理しておきましょう。
| 手法 | 適用する場面 | 効果 |
| :— | :— | :— |
| `as const` | 設定値、定数オブジェクト、配列などを完全にイミュータブルとして扱いたい時 | すべてのリテラル型が維持され、プロパティが `readonly` になる |
| 型注釈 (`: Type`) | 変数には値を再代入する可能性があるが、受け付ける型を厳格に制限したい時 | 指定した型枠に収まるようにバリデーションされる |
TypeScriptの型広げ(Widening)は、一見すると私たちの邪魔をしているように思えるかもしれませんが、実は「安全な再代入」を守るための優しさです。その仕組みを正しく理解し、`as const` や型注釈で手綱を握ってあげれば、あなたの書くコードはより堅牢で、予測可能性の高いものに生まれ変わります。
ここをクリアできれば、TypeScriptの型システムの基本はもうバッチリマスターできていますよ!
ぜひ、今日の開発から意識して使ってみてくださいね。それでは、快適なTypeScriptライフを!