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

こんにちは!TypeScriptの世界へようこそ。先輩エンジニアの言葉を借りて、皆さんの学びの旅をナビゲートしますね。

TypeScriptを書き始めて少し経つと、こんな不思議な経験をすることはありませんか?

「自分は `’admin’` という特定の文字(値)を入れたつもりなのに、TypeScriptの中ではただの `string`(文字列全体)として扱われてしまって、別の場所でエラーになる……」

実はこれ、TypeScriptが持つ「型広げ(Type Widening)」という非常に賢く、そして時に私たちをちょっぴり困らせる仕様が原因なんです。

一見難しそうに聞こえる言葉ですが、仕組みと対策さえ分かってしまえば、TypeScriptを自由自在にコントロールできるようになります。今回は、この「型広げ」の正体と、それを綺麗に固定するためのプロのテクニックを、初学者の方にも分かりやすく丁寧に解説します。「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!」の気持ちで進めていきましょう!

—

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

まずは、イメージしやすい図解的な解説から始めましょう。

TypeScriptは、私たちがコードをすべてガチガチに書かなくても、裏側で「この変数はきっとこういう型だな」と推測してくれます。これを型推論(Type Inference)と呼びます。

この型推論の段階で、「値の範囲をあえて広く解釈する」挙動のことを型広げ(Type Widening)と言います。

イメージ図:型広げのビジュアル

【 狭い世界(リテラル型)】 【 広い世界(プリミティブ型)】
“success” ───(型広げ)───> string
(これ以外は絶対にダメ!) (どんな文字列でもOK!)

なぜTypeScriptはそんなお節介なことをするのでしょうか?
理由は簡単。「変数(`let`)なのだから、後から違う値に書き換えられるかもしれないよね」という優しさ(配慮)から来ているのです。

具体的なコードで、その違いを見てみましょう。

—

2. 決定的な違いを生む `let` と `const`

まずは基本中の基本である、変数の宣言方法による挙動の違いです。

// 1. const で宣言した場合
const userRole = “admin”;
// ⇒ 型推論の結果は: “admin” 型(リテラル型)
// 理由: const(定数)なので、後から値が変わることは絶対にないから!

// 2. let で宣言した場合
let currentStatus = “pending”;
// ⇒ 型推論の結果は: string 型(型広げが発生!)
// 理由: let(変数)なので、後から `currentStatus = “approved”` と書き換える可能性があるから!

`const` で宣言した `userRole` は、二度と書き換えられないので、TypeScriptは極限まで狭い型である `”admin”` 型(これ以外は許さないという超具体的な型)にしてくれます。

一方で、`let` で宣言した `currentStatus` は、後から書き換えることを想定して、一歩引いた広い型(`string`)に自動で「広げ」られます。

これが、型広げの最も基本的なメカニズムです。

—

3. 陥りやすい罠:オブジェクトと配列の「型広げ」

プリミティブ(文字列や数値)の段階では `const` を使えば解決しますが、「オブジェクト(連想配列)」や「配列」になると、話は一気に複雑になります。

ここに、多くの開発者が最初に引っかかる大きな罠があります。

罠①:オブジェクトのプロパティは `const` でも広がる!

// constでオブジェクトを作ったのに…
const config = {
theme: “dark”, // ここは string 型に広がる!
apiPort: 8080 // ここは number 型に広がる!
};

// なぜなら、オブジェクトのプロパティは後から書き換え可能だからです!
config.theme = “light”; // これはエラーになりません。

たとえオブジェクト自体を `const` で宣言しても、中のプロパティ(`theme` や `apiPort`)は後から変更可能です。そのため、TypeScriptは気を利かせて `”dark”` を `string` に、`8080` を `number` に広げてしまいます。

罠②:配列は「タプル(要素数固定)」ではなく「一般的な配列」になる

// 2つの要素を持つ、位置情報のペアを作ったつもり
const coordinates = [35.6812, 139.7671];
// ⇒ 型推論の結果は: number[](数値の配列)
// 理由: 後から要素を増やしたり減らしたり(.pushなど)できるから!

// そのため、本来「要素は2つだけ」のはずなのに、3つ目を追加できてしまいます…
coordinates.push(100.0); // エラーにならない!

このように、配列も「後から変化するもの」として扱われるため、型が大きく広がってしまいます。

—

4. 型広げを掌握する「3つの特効薬」

では、この勝手に広がる型を、自分の意図通りにキュッと固定するにはどうすればいいでしょうか?
現場で毎日使われている、強力な3つの解決策を紹介します。

特効薬①:型注釈(Type Annotation)を明示する

一番ストレートな方法は、自分で「この型だよ!」と宣言(注釈)することです。

// オブジェクトに直接、型を定義してあげる
const config: { theme: “dark” | “light”; apiPort: 8080 } = {
theme: “dark”, // しっかり固定される!
apiPort: 8080
};

// coordinatesを「要素数2つのタプル」として固定する
const coordinates: [number, number] = [35.6812, 139.7671];
// coordinates.push(100); をしようとすると、型の整合性が崩れて後続の処理で守られます

確実ですが、毎回手動で型を書くのは少し面倒ですよね。そこで登場するのが、次の「究極の呪文」です。

特効薬②:最強の魔法 `as const` (const アサーション)

TypeScript 3.4で登場した `as const` は、型広げを完全に制御するための最強の武器です。
値の末尾に `as const` と書くだけで、TypeScriptに対して「これは一切変更しない、完全な定数として扱って!」と命令できます。

// オブジェクトの末尾に as const をつける
const config = {
theme: “dark”,
apiPort: 8080
} as const;

/
裏側での型推論の結果:
readonly theme: “dark”;
readonly apiPort: 8080;
(すべてが読み取り専用になり、型もリテラル型に固定されます!)
/

// 書き換えようとすると、コンパイル時にTypeScriptが全力で守ってくれます!
// @ts-expect-error: エラーが発生します
config.theme = “light”; // Cannot assign to ‘theme’ because it is a read-only property.

配列に対しても同様です。

const coordinates = [35.6812, 139.7671] as const;
// ⇒ 型推論の結果は: readonly [35.6812, 139.7671](中身も長さも固定されたタプル!)

// @ts-expect-error: 要素の追加も防げます
coordinates.push(100); // Property ‘push’ does not exist on type ‘readonly [35.6812, 139.7671]’.

この `as const`、本当に便利ですよね。書くだけで、オブジェクトも配列も一瞬で「不変(Immutable)」な型に固定できます。

特効薬③:読み取り専用型(`readonly`)の活用

関数の引数などで「この配列は中身を書き換えないでね」と明示したい場合は、`readonly` 修飾子を使いましょう。

function printCoordinates(coords: readonly [number, number]) {
console.log(`緯度: ${coords[0]}, 経度: ${coords[1]}`);
// coords[0] = 40.0; // エラーになるので、関数内での誤った書き換えを防げます!
}

—

5. 実践:型広げが引き起こすリアルなバグと解決策

最後に、開発現場で本当によくある「型広げによるバグ」のシチュエーションを見てみましょう。

困ったシチュエーション:APIのステータス送信

以下は、特定のステータスのみを受け取る関数 `sendStatus` がある例です。

// 1. 送信できるステータスを制限(ユニオン型)
type Status = “success” | “pending” | “failed”;

function sendStatus(status: Status) {
console.log(`Status sent: ${status}`);
}

// 2. 送信したいデータをオブジェクトで用意
const requestData = {
id: 101,
status: “success” // ここで型広げが発生! (型は string になってしまう)
};

// 3. 関数に渡そうとすると…
// @ts-expect-error: 型エラーが発生!
sendStatus(requestData.status);
// エラー内容: Argument of type ‘string’ is not assignable to parameter of type ‘Status’.

「あれ? `”success”` を渡しているのに、なんでエラーになるの?」とパニックになりがちですが、もう理由は分かりますよね。`requestData.status` は `string` 型に広がってしまっているため、`Status` 型(3つの文字列しか許さない型)よりも広すぎて、安全ではないと判断されたのです。

スマートな解決策(`as const` で一発解決)

これを解決するには、オブジェクトの定義に `as const` を添えるだけです。

const requestData = {
id: 101,
status: “success”
} as const; // これだけで status は “success” 型に固定されます!

// エラーなく、美しくコンパイルが通ります!
sendStatus(requestData.status);

実務では、このような設定値やAPIへのパラメータなどで `as const` が大活躍します。

—

まとめ:TypeScriptを掌握する第一歩をクリア!

今回のポイントを整理しましょう。

1. 型広げ(Type Widening)は、再代入に備えてTypeScriptが良かれと思って型を「広く解釈する」仕組み。
2. `let` は広がり、`const` は(プリミティブなら)広がらない。
3. オブジェクトや配列は、`const` であっても中身が広がってしまう。
4. 制御するためには、型注釈 を書くか、魔法の呪文 `as const` を末尾に添える。

「型広げ」の性質を理解し、`as const` や型注釈でコントロールできるようになれば、型安全でバグのない堅牢なコードが驚くほどスムーズに書けるようになりますよ。

一歩一歩、確実にTypeScriptの力を手なずけていきましょう。あなたが素晴らしいコードを書けるようになるのを、いつでも応援しています!

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