TypeScriptを用いた開発において、変数の宣言は基本的かつ非常に重要な役割を担っています。

プログラムの中でデータをどのように扱うかを定義する際、JavaScriptから引き継がれたletとconstの適切な使い分けが求められます。

2026年現在のモダンな開発現場では、型安全性を高めるためにこれらの宣言を厳格に管理することが一般的となっています。

単に「再代入ができるかできないか」という点だけでなく、TypeScript特有の型推論の挙動における違いを理解することが、堅牢なコードを書くための鍵となります。

本記事では、letとconstの基本的な違いから、高度な型推論、そして実務で役立つ使い分けの基準までを詳しく解説します。

letとconstの基本的な違いとスコープ

TypeScriptにおいて、変数を宣言する際には主にletとconstの2種類が使用されます。

かつてのJavaScriptで使用されていたvarは、関数スコープや巻き上げといった挙動が混乱を招くため、現在のTypeScript開発で使用されることはまずありません。

ここでは、現代の標準であるletとconstの基本的な性質を整理しましょう。

再代入の可否

letとconstの最も顕著な違いは、値を再代入できるかどうかという点にあります。

letで宣言された変数は、後から別の値を代入することが許可されています。

一方、constで宣言された変数は「定数」として扱われ、一度値を代入すると二度と変更することができません。

この違いをコードで確認してみましょう。

TypeScript
// letは再代入が可能
let counter: number = 0;
counter = 1; // 成功

// constは再代入が不可能
const maxCount: number = 100;
// maxCount = 200; // コンパイルエラー: Cannot assign to 'maxCount' because it is a constant.

このように、値が変化することが予定されている場合はletを使用し、不変であるべき場合はconstを使用するのが基本原則です。

ブロックスコープの適用

letとconstは、どちらもブロックスコープを持ちます。

ブロックスコープとは、{}(波括弧)で囲まれた範囲内でのみ変数が有効になるというルールです。

これより、意図しない場所で変数が参照されるリスクを大幅に軽減できます。

TypeScript
if (true) {
    let blockScopedLet = "I am inside a block";
    const blockScopedConst = "Me too";
    console.log(blockScopedLet);
}

// console.log(blockScopedLet); // エラー: blockScopedLetが見つかりません

このスコープの仕組みにより、プログラムの予測可能性が向上し、デバッグの効率が高まります。

TypeScript特有の型推論における挙動の違い

TypeScriptにおけるletとconstの違いは、単なる再代入の制限にとどまりません。

最も重要な違いの一つは、TypeScriptの型推論エンジンがどのように型を決定するかという点にあります。

letの場合:プリミティブ型への推論(Literal Widening)

letで変数を宣言し、文字列や数値などのリテラル値を代入すると、TypeScriptはその型を一般的なプリミティブ型として推論します。

これは、letで宣言された変数は後から値が変わる可能性があるため、型に幅を持たせる必要があるからです。

TypeScript
let message = "Hello"; 
// 型は string として推論される

この挙動は「Literal Widening」と呼ばれ、変数が他の文字列に書き換えられることを許容するために行われます。

constの場合:リテラル型への推論

対照的に、constで宣言された変数は、その値そのものが型(リテラル型)として推論されます。

constは値が変わらないことが保証されているため、TypeScriptはより厳格な型を割り当てることができるのです。

TypeScript
const status = "success";
// 型は "success" という特定のリテラル型として推論される

この違いは、ユニオン型や関数の引数に特定の値を渡す際に非常に重要になります。

例えば、特定の文字列のみを受け付ける関数に値を渡す場合、constで宣言された変数の方が型安全性が高くなります。

TypeScript
type Mode = "readonly" | "readwrite";

function setMode(mode: Mode) {
    console.log(`Mode set to ${mode}`);
}

const currentMode = "readonly";
let nextMode = "readonly";

setMode(currentMode); // 成功: 型は "readonly"
// setMode(nextMode); // エラー: string型を "readonly" | "readwrite" に割り当てることはできません

このように、constを使用することで、TypeScriptはより詳細な情報を保持し、バグの混入を未然に防いでくれます。

オブジェクトと配列におけるconstの挙動

初心者が最も混乱しやすいポイントの一つが、constで宣言されたオブジェクトや配列の扱いです。

constは「再代入」を禁止しますが、オブジェクトのプロパティや配列の要素の変更まで禁止するわけではありません。

プロパティの変更は可能

以下のコードを見てみましょう。

TypeScript
const user = {
    id: 1,
    name: "Alice"
};

// これは許可される(再代入ではないため)
user.name = "Bob";

// これはエラー(再代入にあたるため)
/*
user = {
    id: 2,
    name: "Charlie"
};
*/

JavaScriptおよびTypeScriptにおけるconstは、その変数が指し示す「参照先」を固定するものです。

参照先であるオブジェクトの中身を書き換えることは制限されないため、注意が必要です。

配列の要素操作

配列についても同様のことが言えます。

constで宣言した配列に対して、pushメソッドなどで要素を追加することは可能です。

TypeScript
const numbers = [1, 2, 3];
numbers.push(4); // 成功

console.log(numbers);
実行結果
[1, 2, 3, 4]

もし、オブジェクトのプロパティや配列の要素も含めて完全に不変(イミュータブル)にしたい場合は、TypeScriptの別の機能を使う必要があります。

as const(const assertion)による厳格化

オブジェクトや配列を完全に定数として扱いたい場合、TypeScript 3.4から導入されたas constを使用するのが最適です。

これを「constアサーション」と呼びます。

as constを付与することで、TypeScriptは以下の挙動を示します。

  • リテラル型がより厳格になる
  • オブジェクトのプロパティがreadonlyになる
  • 配列がreadonlyなタプル型になる
TypeScript
const config = {
    apiUrl: "https://api.example.com",
    timeout: 5000
} as const;

// config.timeout = 10000; // エラー: Cannot assign to 'timeout' because it is a read-only property.

実務では、設定値の定義やマジックナンバーの管理にas constが多用されます。

単なるconst宣言よりも強力に意図を表現できるため、積極的に活用すべき手法です。

いつletを使い、いつconstを使うべきか

開発におけるベストプラクティスは、非常にシンプルです。

デフォルトでは常にconstを使用し、どうしても再代入が必要な場合のみletを検討してください。

constを優先すべき理由

constを多用することには、多くのメリットがあります。

まず、コードを読む人に対して「この変数の値は変わらない」という安心感を与えます。

変数の役割が固定されるため、コードの追跡が容易になり、認知負荷が下がります。

また、先述の通りTypeScriptの型推論を最大限に活かせるため、型チェックの恩恵をフルに受けることができます。

letを使用する正当なケース

一方で、letを使用すべきケースも存在します。

最も一般的なのは、ループ処理の中でのカウンタ変数や、条件分岐によって段階的に値を構築する場合です。

TypeScript
let result: string;
if (success) {
    result = "Success";
} else {
    result = "Failure";
}

ただし、上記のようなケースでも「三項演算子」や「関数化」を利用することで、constに置き換えられることが多いです。

TypeScript
const result = success ? "Success" : "Failure";

このように、可能な限りletを排除する努力をすることで、コードの品質は一段と向上します。

パフォーマンスと最適化の観点

技術的な観点から見ると、letとconstの間で、実行時のパフォーマンスに劇的な差が出ることは稀です。

しかし、現代のブラウザやNode.jsが搭載しているJavaScriptエンジン(V8など)は、constによって値が固定されていることを手がかりに、内部的な最適化を行うことがあります。

また、TypeScriptコンパイラにおいても、constは変数の寿命やスコープを特定しやすいため、より正確な静的解析を可能にします。

性能面での微細なメリット以上に、「開発者による誤操作の防止」という人間的な最適化の効果が非常に大きいです。

letとconstの比較まとめ表

最後に、ここまでの内容を整理した比較表を掲載します。

項目letconst
再代入可能不可能
スコープブロックスコープブロックスコープ
初期化時の必須性任意(undefinedになる)必須
型推論(プリミティブ)一般的な型(string, number等)リテラル型
オブジェクトの中身の変更可能可能(as constがない場合)

まとめ

TypeScriptにおけるletとconstの使い分けは、単なる文法の選択ではなく、プログラムの意図を表現し、型安全性を確保するための重要な設計行為です。

constは再代入を防ぐだけでなく、TypeScriptに対してより具体的な型情報を提供し、堅牢なアプリケーション構築を支えます。

開発の現場では、「変数はデフォルトでconst」というルールを徹底しましょう。

letが必要になったときは、コードの構造を見直すことで、よりクリーンなconstによる記述に変換できないか検討してみてください。

また、オブジェクトや配列の真の不変性を求める場合は、as constを活用することが重要です。

これらの使い分けをマスターすることで、バグが少なく、他のエンジニアにとっても読みやすい高品質なTypeScriptコードを書けるようになります。

日々のコーディングの中で、常に変数の可変性に意識を向け、適切な宣言を選択していきましょう。