【入門編】TypeScriptの「型広げ(Type Widening)」を制御する:推論結果を意図通りに固定するベストプラクティス – TypeScript コア・型システムの基礎解析バイブル

こんにちは!フロントエンドからバックエンドまで、TypeScriptの荒波を一緒に航海する先輩エンジニアです。

今回は、TypeScriptの初学者が必ずといっていいほど最初にぶつかる「型広げ(Type Widening)」という現象について、徹底的に深掘りしていきたいと思います。

「あれ、さっき入れた値なのに、なんだか型が大きくなって怒られる……?」
そんなモヤモヤを感じたことはありませんか?ここをクリアすれば、TypeScriptの推論エンジンを手玉に取れるようになり、コードの安全性と書きやすさが劇的に向上しますよ。

それでは、さっそく「型広げ」の正体と、そのスマートな制御方法を見ていきましょう!

—

1. 「型広げ(Type Widening)」ってなに?

TypeScriptの最大の特徴は、人間が明示的に型を書かなくても、コンパイラが文脈から型を推論してくれる「型推論」の賢さにありますよね。

しかし、この賢さが時に「お節介」になることがあります。それが型広げ(Widening)です。

思考の図解:変数宣言のちがい

私たちがコードを書くとき、`const` で宣言するか、`let` で宣言するかで、TypeScriptの「見方」は180度変わります。

[ const 宣言 ] ──> 「この値は絶対に変わらない!」
└─> 具体的で狭い型(リテラル型)に固定 🔒

[ let 宣言 ] ──> 「後から書き換わるかもしれない…」
└─> 安全のために一般的な型(プリミティブ型)に拡張 🔓

具体例をコードで見てみましょう。

// constで宣言した場合
const directionConst = “left”;
// 型: “left” (”left”という値そのものが型になっている = 文字列リテラル型)

// letで宣言した場合
let directionLet = “left”;
// 型: string (”left”の可能性もあるし、”up”や”hello”かもしれないので string に広げられる)

`let`で宣言された瞬間、TypeScriptは「あとから `directionLet = “right”` と書き換えられるかもしれない」と予測し、型を `string` という広大な宇宙へ広げてしまいます。これが型広げです。

—

2. 陥りがちな罠:なぜ型広げでコードが壊れるのか?

実際の開発現場では、この型広げが原因で「意図しないバグ」や「コンパイルエラー」を引き起こすことがあります。

例えば、関数の引数に「特定の文字列のいずれか」を要求したい場合を考えてみましょう。

type Theme = “light” | “dark”;

function setTheme(theme: Theme) {
console.log(`Setting theme to ${theme}`);
}

// ユーザーの入力値を管理する変数があるとします
let userSelectedTheme = “light”;
// おっと! let なので、userSelectedTheme の型は “light” ではなく string に広げられています!

// これを関数に渡そうとすると…?
setTheme(userSelectedTheme);
// ❌ コンパイルエラー!
// 理由: 引数は “light” | “dark” を求めているのに、
// userSelectedTheme は広すぎる string 型だから渡せないよ!

「いやいや、中身は `”light”` なんだから渡させてよ!」と思いますよね。でも、TypeScriptは未来の書き換え(`userSelectedTheme = “hoge”` など)を心配して、厳格に止めてくるのです。

—

3. 型広げを意図通りに制御する 2つのベストプラクティス

この「大きくなりすぎる型」を飼い慣らし、意図通りに固定するためのアプローチが主に2つあります。状況に合わせて使い分けましょう。

方法A:型注釈(Type Annotation)で最初に境界線を引く

変数を宣言する際、あらかじめ「この型でいくんだ」と明示的に指定(注釈)する方法です。

type Theme = “light” | “dark”;

// 型注釈をつけて let を宣言する
let userSelectedTheme: Theme = “light”;

// これなら型は最初から “light” | “dark” に固定されているので…
setTheme(userSelectedTheme); // ⭕️ 完璧に通ります!

【ここがポイント】
型注釈を書いておけば、TypeScriptは「あ、この変数は広げなくていいんだな」と理解し、型広げを行いません。

—

方法B:`as const`(constアサーション)で究極のイミュータブルを召喚する

オブジェクトや配列、あるいはプリミティブな値に対して、「この構造と値は一歩も変えさせない!」とコンパイラに誓わせるのが `as const` です。

// 普通のオブジェクト定義
const config = {
endpoint: “https://api.example.com”,
timeout: 5000,
};
// 型推論: { endpoint: string; timeout: number; }
// ※プロパティの値を後から書き換えられるため、型が広げられています。

// constアサーションを使ったオブジェクト定義
const strictConfig = {
endpoint: “https://api.example.com”,
timeout: 5000,
} as const;
/ 型推論:
{
readonly endpoint: “https://api.example.com”;
readonly timeout: 5050ではなくて 5000;
}
/

`as const` を使うと、以下の魔法がかかります:
1. すべてのプロパティが `readonly`(読み取り専用)になる。
2. 文字列や数値が「その値そのものの型(リテラル型)」に固定される。
3. 配列は `readonly [値1, 値2]` という「タプル型」になる。

APIのエンドポイント一覧や、ステータスの定義など、「定数としてガチガチに固定したいデータ」には迷わず `as const` を使いましょう。

—

4. まとめ:型広げを制する者はTypeScriptを制す

今回は、TypeScriptの裏側でこっそり行われている「型広げ」の仕組みと、その制御方法を解説しました。

  • `let` を使うと、変数の型は安全のために広く推論(広げられ)てしまう。
  • 厳密な型として扱いたい場合は、「型注釈」で型をあらかじめ制限する。
  • 値や構造を完全に固定・読み取り専用にしたい場合は `as const` を使う。

「なぜここでエラーが出るんだろう?」と思ったときは、エディタで変数にマウスカーソルを合わせ、TypeScriptがその変数をどんな型として見ているか(広げられていないか)を確認してみてください。

ここをクリアすれば、TypeScriptの基本はバッチリマスターできていますよ!自信を持って次のステップへ進みましょう。それでは、また次回の記事でお会いしましょう!

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