変数スコープを自在にコントロールできるかどうかは、TypeScriptで複雑なロジックを書き始めたときに、生産性とバグ発生率を大きく左右する重要なポイントです。
特に非同期処理やイベントハンドラ、DIコンテナ、状態管理などが絡んでくると、「どのタイミングで、どの値が、どこから参照されているのか」を正しく把握できないと、意図しない挙動や思わぬ副作用に悩まされがちです。
そこで鍵になる概念がクロージャです。
クロージャを理解すると、「関数が定義されたときの環境を、その関数が実行されるずっと後のタイミングでも保持し続けられる」という性質を、意図的に活用できるようになります。
TypeScriptにおけるクロージャは、単に「JavaScriptの仕様の一部」ではなく、型情報と組み合わせることで、より安全で表現力の高い抽象を作るための強力なツールになります。
例えば、外側のスコープに配置した設定値やカウンタ、キャッシュを、内側の関数からだけ参照可能にすることで、APIをシンプルに保ちながら内部実装を厳密にカプセル化できます。
また、関数を返す関数を適切に設計すれば、意図したスコープだけを「閉じ込めた」ミニマルなステートフルコンポーネントのようなものを関数ベースで定義でき、オブジェクト指向に寄りかからない実装スタイルも取りやすくなります。
クロージャの振る舞いを誤解したまま実務コードを書くと、ループ変数のキャプチャミスや、使い終わったはずの変数への参照が残り続けることで、予期せぬメモリ消費やバグを生み出す可能性があります。
一方で、その性質を理解した上で活用すれば、設定値を外出ししたカスタマイズ可能なユーティリティ関数や、一時的な状態を持つイベントハンドラ、ミドルウェア的な処理チェーンなどを、簡潔かつテストしやすい形で構築できます。
function createCounter(start: number = 0) {
let value = start;
return () => ++value;
}
const counterA = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
上のようなシンプルな例でも、「createCounterが定義された環境のvalue変数を、返された関数が永続的に保持し、呼び出しのたびに変化させている」というクロージャの本質が表れています。
この記事では、このクロージャの仕組みをTypeScriptの型システムと絡めながら丁寧に分解し、次のような実務で頻出のシーンに落とし込んでいきます。
- 設定値や依存オブジェクトを外出しした関数ファクトリで、疎結合なロジックを組み上げる
- イベントハンドラやコールバックの中で、外側のスコープを安全に参照しつつ副作用を限定する
- 非同期処理や再試行ロジックの中で、試行回数や状態をクロージャに閉じ込める
変数スコープとクロージャの関係を正しく理解し、TypeScriptでの具体的な活用パターンを押さえておくことで、読みやすく保守しやすい関数指向のコードを書きやすくなります。
この記事を通じて、クロージャを「なんとなく動いている仕組み」から「意図的に設計して再利用できる抽象」へと昇華し、日々の開発での武器として使いこなせるようになることを目指します。
- TypeScriptのクロージャとは?変数スコープから理解する基礎概念
- 変数スコープを整理する:グローバル・関数・ブロックスコープの違い
- TypeScriptでクロージャが生まれる瞬間:実行コンテキストとレキシカル環境
- クロージャの典型パターン集:カウンタ・設定値・キャッシュをスコープに閉じ込める
- 実務でよくある落とし穴:ループ変数のキャプチャとメモリリークを防ぐTypeScriptの書き方
- イベントハンドラと非同期処理でクロージャを安全に使う設計パターン
- TypeScriptの型システムでクロージャを強化する:関数ファクトリと依存性注入
- 関数型スタイルで考えるクロージャ:状態管理とテスト容易性を両立する設計
- TypeScriptクロージャのベストプラクティス:保守しやすい変数スコープ設計のまとめ
- クロージャを直感的に理解するための簡単な例
- スコープチェーンとシャドーイングを意識したコード設計
- ブロックスコープとvar/let/constの選び方
- 実行コンテキストスタックとクロージャのライフサイクル
- クリーンなクロージャを保つためのスコープ分割テクニック
- カウンタ関数で学ぶミニマルな状態管理
- 設定値を外出しした関数ファクトリのクロージャパターン
- キャッシュをクロージャに閉じ込めることで得られるメリット
- forループと非同期処理で起きる典型的なクロージャバグ
- letとconstを使った安全なループ変数のキャプチャ方法
- 不要な参照を残さないためのクリーンアップ戦略
- DOMイベントハンドラの中で状態を持たせるときの注意点
- Promiseとクロージャを組み合わせた再試行ロジック
- イベント駆動アーキテクチャでのクロージャ活用パターン
- 型付きクロージャで依存関係を明示するメリット
- DIコンテナとクロージャを組み合わせたTypeScript設計
- 関数型スタイルでの状態管理とクロージャの相性
- テスト容易性を高めるクロージャの切り出し方
- オブジェクト指向とクロージャの使い分け戦略
TypeScriptのクロージャとは?変数スコープから理解する基礎概念

クロージャを理解するための一番確実な入り口は、「関数と変数スコープの関係」を丁寧に押さえることです。
クロージャそのものは難しい特殊機能ではなく、「関数が定義されたときのスコープ情報を、関数が実行されるタイミングまで持ち越す仕組み」と捉えると、構造的に理解しやすくなります。
TypeScriptはJavaScriptをベースにした言語ですので、ランタイムとしてはJavaScriptのクロージャの挙動をそのまま引き継ぎつつ、型情報によってその挙動をより安全に扱えるようにしていると考えるとよいです。
まず押さえるべき前提として、TypeScript(JavaScript)の変数スコープには、ざっくりと次のような種類があります。
グローバルスコープ、関数スコープ、ブロックスコープです。
varは関数スコープ、letとconstはブロックスコープを持ち、モジュールのトップレベルに書かれた変数はモジュールスコープとして扱われます。
クロージャはこれらのスコープがどのように入れ子になっているか、そして「関数がどこで定義されたか」を基準に変数を解決するという性質から自然に生まれます。
ここで重要になるのが「レキシカルスコープ」という概念です。
レキシカルスコープとは、コードが書かれた位置(字面)に基づいてスコープを決める方式であり、TypeScriptの関数はこのルールに従って外側の変数へアクセスします。
つまり、関数がどこで「呼び出されるか」ではなく、「どこで定義されるか」によって、その関数から見える変数の集合が決まります。
この性質があるからこそ、関数を返したり、他のスコープへ渡したりしても、定義時点で参照していた変数に継続してアクセスできるわけです。
クロージャをもう少し形式的に説明すると、「関数オブジェクト+その関数が参照する外側の変数環境」の組み合わせです。
関数が生成されるとき、その関数がアクセス可能な外側のスコープの情報が内部的なデータ構造として保持されます。
その後、関数が呼び出されるたびに、この保持されている変数環境を見にいくことで、関数定義時点と同じように外側の変数を読み書きできるようになっています。
この「保持される外側の変数環境」が、いわゆるクロージャの本体だと捉えてください。
具体的なイメージを持つために、少しだけコードを見てみます。
function createMultiplier(factor: number) {
return (value: number) => value * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30
この例では、createMultiplierがfactorという変数を受け取り、その変数を参照する関数を返しています。
doubleはfactor = 2という環境を、tripleはfactor = 3という環境を、それぞれクロージャとして保持しています。
ここで重要なのは、createMultiplierの呼び出しが終わったあとでも、factorが生き続けている点です。
通常、関数のローカル変数は関数の実行が終われば破棄されるイメージですが、クロージャが存在する場合、その変数が参照され続ける限り環境は保持されます。
このような振る舞いを理解するうえで、「いつの時点の値が参照されるのか」を意識することが重要です。
上の例では、factorはcreateMultiplierの呼び出しごとに別々の値を取り、その値がクロージャに固定されます。
つまり、doubleとtripleは同じ関数形を持ちながらも、異なる環境を持つ別々のクロージャです。
これは、まるで同じ設計図から作られた2つのオブジェクトが、それぞれ別のフィールド値を持っている状況に近いですが、クロージャではそれを「関数+環境」という形で実現しています。
一方で、外側の変数を「共有する」ようなクロージャもあります。
次のような例を考えてみます。
function createSharedCounter() {
let count = 0;
return {
inc: () => ++count,
get: () => count
};
}
const counter = createSharedCounter();
counter.inc();
counter.inc();
console.log(counter.get()); // 2
このコードでは、incとgetという2つの関数が同じcount変数を参照しています。
両方の関数が同じ外側のスコープを共有しているため、incによる更新結果がgetからそのまま見えるようになっています。
ここで起きているのは、「1つの変数環境を複数の関数が共有する」タイプのクロージャです。
このような構造を理解しておくと、状態をどこに持たせ、どの関数からアクセスさせるべきかを設計する際に、より意図的なスコープ設計ができるようになります。
TypeScriptという観点で重要なのは、これらのクロージャを型で表現できる点です。
返り値の関数がどのようなパラメータを受け取り、どのような型の値を返すのかを明示することで、クロージャが保持する環境の操作に対する誤りをコンパイル時に検出できます。
たとえば、factorが数値であることを前提にしたクロージャであれば、誤って文字列を渡そうとした瞬間にコンパイルエラーになります。
クロージャ自体のランタイム挙動はJavaScriptと同じですが、型情報によって「どういう使われ方を期待しているか」を厳密に宣言できるのがTypeScriptの強みです。
さらに、変数スコープの観点からクロージャを見ると、「外側のスコープをどれだけ狭く、どれだけ明示的に設計するか」が保守性に直接影響します。
必要以上に多くの変数を1つのスコープに閉じ込めると、どの関数がどの変数に依存しているのかが不透明になり、テストやリファクタリングの難度が上がります。
逆に、必要な情報だけを最小限のスコープに閉じ込め、そのスコープを跨いだ依存を減らすことで、クロージャは「小さな状態を持つコンポーネント」のように扱えるようになります。
最後に、クロージャという概念を「魔法のような挙動」と捉えず、「レキシカルスコープと関数オブジェクトの組み合わせによる必然的な結果」として理解することが重要です。
変数スコープの基本と、関数がどこで定義されるかという視点をセットで考えることで、クロージャは直感的に説明可能な仕組みになります。
そして、TypeScriptではその直感的な理解に型という追加の軸が乗るため、より安全かつ表現力の高い抽象としてクロージャを活用できるようになります。
変数スコープを整理する:グローバル・関数・ブロックスコープの違い

クロージャを正しく理解するためには、その前提として「変数スコープの種類」をきちんと整理しておく必要があります。
TypeScript(JavaScript)の世界で変数スコープを曖昧なまま捉えていると、どこからどこまでが見えていて、いつ破棄されるのかという基本的な挙動でつまずきやすくなります。
特に、グローバルスコープ、関数スコープ、ブロックスコープの違いを明確に区別できているかどうかは、クロージャがどの変数を捕まえているのかを説明するときに、そのまま論理の土台になります。
まず、グローバルスコープについて整理します。
ブラウザ環境であればwindow、Node.js環境であればglobalやglobalThisに紐づく領域がいわゆるグローバルスコープです。
TypeScriptでモジュールを使わずにスクリプトファイルを直接読み込むような構成では、ファイル先頭に書いた変数がグローバルに露出します。
一方、標準的なTypeScriptプロジェクトでは、ファイルはモジュールとして扱われることが多く、トップレベルの変数は「モジュールスコープ」に閉じ込められ、他のファイルからは直接参照されません。
とはいえ、「どこからでも参照できるような広いスコープ」に変数を置くことの是非を考えるうえで、グローバルスコープの存在を意識しておくことは重要です。
グローバルに近いスコープに値を置くと、たしかにどこからでも使える便利さがありますが、依存関係が見えづらくなりやすいというデメリットもあります。
クロージャの観点で見ると、グローバルスコープの変数は特定のクロージャに閉じ込められるわけではなく、単に「誰からでも見える共有状態」として扱われます。
そのため、意図しない箇所から更新が入り、関数の振る舞いが外側のグローバル状態に強く影響されるような設計になりがちです。
クロージャを活用したい場面では、むしろグローバルスコープに頼らず、より局所的なスコープに状態を閉じ込める方が、テスト容易性や保守性の面で有利になります。
次に、関数スコープについてです。
JavaScriptで古くから存在するvarキーワードは関数スコープを持ちます。
つまり、varで宣言された変数は、その変数が書かれている関数全体から参照可能であり、ブロック境界(ifやforなど)ではスコープが分割されません。
これは、C系言語に慣れていると直感に反する挙動を生みやすいポイントです。
一方で、TypeScriptではletとconstが推奨されており、これらはブロックスコープを持つため、関数スコープとブロックスコープを意識的に使い分ける必要があります。
関数スコープは、クロージャの観点から見ると「クロージャの最小単位」として捉えることができます。
ある関数の中で宣言された変数は、その関数の内側で定義される別の関数から参照されることで、クロージャの環境として保持されます。
つまり、「外側の関数のローカル変数」がクロージャによって長生きする対象です。
ですので、関数スコープに置く変数は「この関数と、その内側で定義される関数たちから参照されてもよいものか」を意識して設計する必要があります。
ブロックスコープは、letとconstが導入されたことによって、JavaScript(TypeScript)の変数スコープ設計に新たな軸を与えました。
ブロックスコープとは、{}で囲まれた範囲ごとに独立したスコープが作られ、その内側で宣言された変数は外側から見えなくなるという仕組みです。
if文、forループ、whileループなどはブロックスコープを形成します。
これにより、例えばループごとに別々の変数インスタンスを持つことができるようになり、非同期処理やイベントハンドラと組み合わせたときの挙動が、関数スコープだけだった頃に比べてはるかに直感的になりました。
少し具体的なコードで、関数スコープとブロックスコープの違いを見てみます。
function demoScopes() {
var x = 1;
if (true) {
var x = 2;
let y = 3;
console.log("inside block x:", x); // 2
console.log("inside block y:", y); // 3
}
console.log("outside block x:", x); // 2
// console.log("outside block y:", y); // コンパイルエラー
}
この例では、var xは関数スコープなので、ifブロックの中で再代入すると関数全体のxが書き換わります。
一方、let yはブロックスコープなので、ifブロックの外側からは参照できません。
同じ関数の中にありながら、ブロックスコープの変数は内側に閉じ込められるため、意図しない箇所からのアクセスを防ぐことができます。
クロージャが閉じ込める対象も、このブロックスコープをまたいでどの変数が見えるかによって変わってきます。
ブロックスコープの存在は、クロージャの設計にとって非常に重要です。
例えば、forループの中でイベントハンドラや非同期処理を登録するとき、varを使っていると全てのハンドラが同じループ変数を参照してしまうという典型的なバグが発生します。
しかし、letを使えば、ループごとに異なる変数インスタンスがブロックスコープに閉じ込められ、それぞれのクロージャが独立した値を保持するようになります。
この違いは、クロージャを前提としたロジックを設計するときに、バグと正しい挙動の分岐点になります。
ここまでの内容を整理すると、変数スコープに関して次のような構図が見えてきます。
- グローバル(あるいはモジュール)スコープは最も広く、どこからでも参照され得る共有状態を持つ
- 関数スコープは、関数のローカル状態と、そこから生まれるクロージャの環境の単位になる
- ブロックスコープは、さらに細かく局所的な状態を閉じ込めるための単位であり、ループや条件分岐ごとに独立した変数を持つための仕組みになる
クロージャを設計するときには、どのスコープの変数を閉じ込めるのか、そして閉じ込めた変数がどのくらいの広さを持つべきかを意識することが重要です。
グローバルスコープの変数をクロージャに閉じ込めても、それはあまり意味がありません。
なぜなら、その変数は他のどこからでも更新可能であり、「クロージャによる局所的な状態管理」という利点が薄れてしまうからです。
一方で、関数スコープやブロックスコープに置かれた変数をクロージャで閉じ込めることで、その関数群だけがアクセスできる小さな状態空間を作ることができ、これは設計の自由度を大きく高めます。
TypeScriptでは、これらのスコープの違いを型と併せて考えることで、より安全なコードを書けます。
例えば、グローバルに置かれたオブジェクトはどこからでも書き換えられるため、型レベルでは整合性が取れていても、実行時には予期しない状態に変更されるリスクがあります。
対照的に、関数スコープやブロックスコープに閉じ込めた不変オブジェクトをクロージャが参照するように設計すれば、そのオブジェクトがどこから更新され得るかが明確になり、状態遷移の追跡が容易になります。
結局のところ、グローバル・関数・ブロックスコープの違いを理解することは、「どの変数をどのスコープに置き、どの関数から参照できるようにするか」という設計判断を論理的に行うための基盤です。
クロージャはその設計の上に成り立つ仕組みですので、スコープの違いを曖昧にしたままクロージャを使おうとすると、内部で何が起きているのかを説明しにくくなります。
逆に、スコープの構造をきちんと意識したうえでクロージャを設計すれば、「どの関数がどの状態に依存しているのか」を明快に語れるコードになり、長期的な保守にも耐えやすくなります。
TypeScriptでクロージャが生まれる瞬間:実行コンテキストとレキシカル環境

クロージャを本質的に理解するためには、「いつ」「どのようにして」クロージャが生まれるのかを、実行コンテキストとレキシカル環境という観点から分解して考える必要があります。
TypeScriptはコンパイル後にJavaScriptとして実行されますので、ランタイムの挙動はJavaScriptの実行モデルに従います。
その中心にある概念が実行コンテキスト(Execution Context)とレキシカル環境(Lexical Environment)です。
この2つを押さえることで、クロージャが単なる「関数が外側の変数を覚えてくれる便利な仕組み」ではなく、「特定のタイミングで固まるスコープ情報のスナップショット」であることが見えてきます。
まず、実行コンテキストとは何かを整理します。
実行コンテキストは、ざっくり言えば「あるコード片が実行されているときの環境情報をまとめたもの」です。
グローバルコードが実行されているときにはグローバルコンテキスト、関数が呼び出されるときには関数コンテキストが作られ、それらがスタック状に積み上がります。
各実行コンテキストは、「どの変数が見えるか」「thisは何か」「外側のスコープへの参照は何か」といった情報を持っています。
このうち、変数スコープに関する情報を管理しているのがレキシカル環境です。
レキシカル環境は、「そのコンテキストで有効な識別子(変数・関数名など)と、それに対応する値を保持するオブジェクト」と、「外側のレキシカル環境への参照」の組み合わせで構成されます。
イメージとしては、「現在のスコープの辞書」と「一つ外側の辞書へのポインタ」が連なっているような構造です。
コードが実行されるとき、変数の名前を解決する際には、このレキシカル環境チェーンを辿っていき、最初に一致するエントリを探します。
これがレキシカルスコープに基づく名前解決の仕組みです。
クロージャが生まれる瞬間は、「関数オブジェクトが生成されるとき」に訪れます。
関数宣言や関数式、アロー関数などが評価されると、新しい関数オブジェクトが作られますが、その際に「その関数が定義されたレキシカル環境への参照」が関数オブジェクトの内部に埋め込まれます。
ここが極めて重要です。
関数は実行される場所ではなく、定義された場所のレキシカル環境を覚えます。
この参照が、後にクロージャとして振る舞うための土台になります。
イメージを具体化するために、少しだけコードで見てみます。
function createLogger(prefix: string) {
const base = `[${prefix}]`;
return (message: string) => {
console.log(base, message);
};
}
const appLogger = createLogger("App");
appLogger("started");
このコードが実行されるとき、createLoggerが呼び出された瞬間に関数コンテキストが生成され、その中にレキシカル環境が作られます。
このレキシカル環境には、prefixとbaseという変数が登録されています。
そして、return行に到達したとき、アロー関数が評価され、新しい関数オブジェクトが生成されます。
この生成のタイミングで、「今まさに存在しているレキシカル環境(prefixとbaseを持つ環境)への参照」がアロー関数の内部に格納されます。
これが、まさにクロージャが生まれる瞬間です。
その後、createLoggerの実行コンテキストはスタックから取り除かれますが、レキシカル環境自体は即座には破棄されません。
なぜなら、その環境を参照している関数オブジェクト(ここではappLogger)がまだ生きているからです。
結果として、appLoggerが呼び出されるたびに、関数は自分の内部に持っている「定義時のレキシカル環境」へアクセスし、そこに格納されているbaseを取得してログ出力に利用します。
このようにして、クロージャは「関数と、その関数が定義されたときのレキシカル環境の組み合わせ」として動作します。
この流れを少し抽象化すると、次のようなプロセスになります。
- グローバルまたは関数コンテキストの中でレキシカル環境が作られる
- その環境内で関数が定義される(宣言/式/アロー関数)
- 関数オブジェクト生成時に「現在のレキシカル環境への参照」が関数に紐づく
- 外側のコンテキストが終了しても、関数オブジェクトが残っている限りレキシカル環境も保持される
- 関数呼び出し時に、その参照経由でレキシカル環境が利用される
ここでポイントになるのは、「クロージャは呼び出し時ではなく、定義時に決まる」という点です。
関数をどこへ渡そうが、どこで呼び出そうが、その関数に紐づいているレキシカル環境は変わりません。
これが、イベントハンドラやコールバック、タイマー関数などを使う際に「どの値が捕まるのか」を説明するときの軸になります。
もう少し複雑な例として、レキシカル環境が入れ子になるケースを見てみます。
function outer(value: number) {
const doubled = value * 2;
function inner(offset: number) {
const result = doubled + offset;
return result;
}
return inner;
}
const fn = outer(10);
console.log(fn(5)); // 25
このコードでは、outerの実行コンテキストに対応するレキシカル環境と、innerの実行コンテキストに対応するレキシカル環境が階層的に存在します。
inner関数オブジェクトが生成されるとき、「innerが定義された場所のレキシカル環境への参照」、すなわち「doubledを含むouterのレキシカル環境への参照」が紐づきます。
fn呼び出し時には、新しい実行コンテキストが作られ、そのコンテキストのレキシカル環境にはoffsetが格納されます。
そして、result計算時にdoubledを参照するために、innerに紐づいている外側のレキシカル環境を辿る、という処理が行われます。
このとき、outerの呼び出しが終わっていても、そのレキシカル環境はfnが生きている限り保持されます。
つまり、outer(10)を呼び出した瞬間に「doubled = 20を持つクロージャ」が生成され、それがfnとして外側に渡されていると解釈できます。
ここで重要なのは、outerを複数回呼び出せば、そのたびに異なるレキシカル環境を持つクロージャが生成されるという点です。
例えば、outer(5)とouter(10)は、それぞれ異なるdoubled値を持つ環境を形成し、これらは互いに独立した状態として振る舞います。
TypeScriptの観点では、これらのレキシカル環境への参照は型には直接現れませんが、返り値の型やパラメータの型を通じて間接的に表現されます。
outerの返り値が(offset: number) => numberであることを型で宣言することで、「この関数はoffsetを受け取り数値を返すが、その内部では外側の数値(doubled)を利用する」という契約を明示できます。
型はレキシカル環境自体の構造を記述してはいませんが、「どのような引数と戻り値の組み合わせで、その環境を利用するか」というインターフェースを示していると見ることができます。
さらに、実行コンテキストとレキシカル環境の関係を理解しておくと、クロージャのライフタイムを意識した設計がしやすくなります。
あるクロージャが長期間保持されると、そのレキシカル環境も長生きします。
その環境の中に大量のオブジェクト参照や外部リソースへのハンドルが含まれている場合、不要なメモリ使用やリソースリークの原因になり得ます。
クロージャの生成タイミング、そしてどのスコープのレキシカル環境を捕まえているのかを意識することで、「どこでクロージャを作るか」「どこで破棄するか」といった設計判断を論理的に行えるようになります。
まとめると、TypeScriptでクロージャが生まれる瞬間は、「関数オブジェクトが生成され、同時にその関数が定義されたレキシカル環境への参照が紐づくタイミング」です。
実行コンテキストはその都度入れ替わりますが、レキシカル環境はクロージャを通じて寿命を延ばされることがあります。
このメカニズムを理解しておくことで、単に「外側の変数にアクセスできる」という現象論ではなく、「どの時点のスコープ情報がどの関数に紐づくのか」という構造論としてクロージャを説明できるようになります。
そしてそれが、TypeScriptでクロージャを安全かつ意図的に活用するための土台になります。
クロージャの典型パターン集:カウンタ・設定値・キャッシュをスコープに閉じ込める

クロージャの挙動を抽象的に理解したあとで次に押さえたいのが、「どのようなパターンで使うと実務的に役に立つのか」という具体的な利用例です。
クロージャは理論的には「関数とレキシカル環境の組み合わせ」ですが、現場レベルでは、カウンタ、設定値、キャッシュなどの状態をスコープに閉じ込めるための道具として頻出します。
これらのパターンは、TypeScriptでサービスクライアントやユーティリティ関数、イベントハンドラ、ミドルウェア的なロジックを設計するときに、ほぼ必ず出会うと言ってよいです。
まず最も分かりやすいのが「カウンタ」をクロージャに閉じ込めるパターンです。
これは、純粋なインクリメント処理を外部に晒すのではなく、カウンタの内部状態を関数スコープに隠しつつ、操作用のインターフェースだけを返す構造になっています。
function createCounter(start = 0) {
let value = start;
return {
inc: () => ++value,
dec: () => --value,
get: () => value
};
}
const counter = createCounter(10);
counter.inc(); // 11
counter.dec(); // 10
console.log(counter.get()); // 10
この例では、valueがカウンタの内部状態として関数スコープに閉じ込められています。
外側からはinc、dec、getというメソッドを通してしか状態にアクセスできません。
クロージャがなければ、このような状態の隠蔽をクラスで行うか、グローバルな変数を直接操作してしまう可能性がありますが、クロージャを使うことで、より小さく局所的な状態管理の単位を関数レベルで作ることができます。
TypeScriptの型システムと組み合わせることで、startの型や戻り値の型を明示できるため、このカウンタを使うコードの安全性も保てます。
次に、「設定値」をクロージャに閉じ込めるパターンを見てみます。
設定値とは、APIのエンドポイントやタイムアウト値、ログの出力レベルなど、複数の処理から共有されるけれども頻繁には変えたくないパラメータです。
これをグローバルな定数として持つのではなく、関数スコープに閉じ込めておくことで、依存関係の注入とカスタマイズを柔軟に行えるようになります。
type HttpConfig = {
baseUrl: string;
timeoutMs: number;
};
function createHttpClient(config: HttpConfig) {
return async (path: string): Promise<Response> => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
try {
const res = await fetch(config.baseUrl + path, {
signal: controller.signal
});
return res;
} finally {
clearTimeout(timer);
}
};
}
const client = createHttpClient({
baseUrl: "https://api.example.com",
timeoutMs: 5000
});
client("/users");
ここでは、configオブジェクトがクロージャによってcreateHttpClientのスコープに閉じ込められています。
返された関数は、どこで呼び出されても同じ設定値を参照しますが、設定値の具体的な内容は外側から直接触れられません。
必要なら別の設定値でクライアントをもう一つ作ることもできますし、テスト用にモックの設定値を渡すことも容易です。
重要なのは、設定値がグローバルにばら撒かれているのではなく、「このクライアント関数のためだけに閉じ込められている」というスコープの構造です。
設定値をクロージャに閉じ込めるパターンは、ログ出力や認証トークンの扱いでもよく使われます。
例えば、以下のようにログレベルやタグを設定として閉じ込めることで、呼び出し側はログ関数の詳細を意識せずに利用できます。
type LogLevel = "debug" | "info" | "warn" | "error";
function createLogger(level: LogLevel, tag: string) {
return (message: string) => {
const formatted = `[${tag}] (${level}) ${message}`;
console.log(formatted);
};
}
const debugLogger = createLogger("debug", "UserService");
debugLogger("fetch started");
この例では、levelとtagという設定値がクロージャに閉じ込められています。
debugLoggerはそれらを常に同じ値として扱い、呼び出し時の引数はmessageだけになります。
設定値をクロージャに閉じ込めることで、パラメータの数を減らしつつ、依存関係を明示した設計が可能になります。
3つめの典型的なパターンが「キャッシュ」をクロージャに閉じ込めるパターンです。
キャッシュは、計算コストの高い処理やネットワークアクセスの結果を再利用するために使われますが、その状態をどこに持つかは設計上の重要な問題です。
グローバルにキャッシュを置くと、どの関数がいつそれを読み書きしているのかが不透明になりやすくなります。
クロージャを使うと、特定の関数群だけがアクセスできる局所的なキャッシュを作ることができます。
function createCachedFetcher<T>(fetcher: () => Promise<T>) {
let cached: T | undefined;
let fetched = false;
return async () => {
if (fetched) {
return cached as T;
}
const result = await fetcher();
fetched = true;
cached = result;
return result;
};
}
const fetchConfigOnce = createCachedFetcher(async () => {
const res = await fetch("/config.json");
return res.json();
});
このコードでは、cachedとfetchedという2つの変数がクロージャに閉じ込められています。
返された関数は、初回呼び出し時にfetcherを実行し、その結果をキャッシュとして保存します。
2回目以降は、ネットワークアクセスを行わずにキャッシュされた結果を返します。
ここで重要なのは、キャッシュのライフタイムがcreateCachedFetcherから返された関数と一致している点です。
別のキャッシュが必要であれば、別のクロージャを生成すればよく、互いのキャッシュ状態は独立しています。
キャッシュをクロージャに閉じ込めるパターンは、設定ファイルや認証情報の読み込み、辞書データのロードなど、さまざまな場面で活用できます。
TypeScriptの型を併用することで、キャッシュが保持する値の型を明確にし、誤った利用をコンパイル時に防ぐことができます。
例えば、createCachedFetcher<string>のようにジェネリクスを明示しておけば、キャッシュされた値が文字列であることを前提にした処理を書くことができます。
これらのパターンに共通しているのは、「状態をグローバルに晒さず、クロージャによってスコープに閉じ込める」という設計方針です。
カウンタは数値の状態、設定値は構成情報、キャッシュは計算結果やレスポンスといった異なる種類の状態を扱っていますが、どれも「関数スコープに閉じ込め、操作用の関数だけを外に渡す」という構造を持っています。
クロージャを使うことで、オブジェクト指向的なクラス設計に頼らずとも、小さな状態を持ったコンポーネントのようなものを関数ベースで構築できるわけです。
実務でこれらのパターンを使うときに注意したいのは、「どのくらいの粒度で状態を閉じ込めるか」というスコープ設計です。
カウンタなどの純粋に局所的な状態は、1つのクロージャにまとめてしまって問題ありません。
しかし、設定値やキャッシュのように複数のコンポーネントから参照される可能性がある状態については、「1つの巨大なクロージャにすべてを閉じ込める」のではなく、「利用単位ごとに小さなクロージャを作る」という方針の方が保守性は高くなります。
例えば、HTTPクライアント用の設定とログ用の設定を別々のクロージャに分けておけば、それぞれを独立にテスト・差し替えできます。
また、キャッシュをクロージャに閉じ込める場合、ライフタイムを意識する必要があります。
長く生きるクロージャに大きなデータ構造をキャッシュすると、メモリ消費が増え続ける可能性があります。
一方で、短命なクロージャではキャッシュのメリットが十分に発揮されないかもしれません。
このようなトレードオフを判断するためにも、「クロージャの寿命=その関数オブジェクトの寿命」という基本を意識し、どこで関数を保持し続けるかを設計段階で考えることが重要です。
まとめると、カウンタ・設定値・キャッシュといった典型的なパターンは、クロージャの実務的な価値を最もわかりやすく示してくれます。
TypeScriptでは、これらをただ「動けばよい」形で書くだけでなく、型情報によって期待する挙動と制約を明示しながら設計できるため、長期的な保守にも耐えやすい抽象として育てることができます。
クロージャを使って状態をスコープに閉じ込めるという発想は、小さなユーティリティからサービスレイヤーまで幅広く応用できるので、まずはここで紹介した典型パターンを自分のプロジェクトの中に少しずつ取り入れていくとよいと思います。
実務でよくある落とし穴:ループ変数のキャプチャとメモリリークを防ぐTypeScriptの書き方

クロージャを実務で使う際に、特に注意が必要になるのが「ループ変数のキャプチャ」と「メモリリーク」の問題です。
どちらもクロージャの仕組み自体は正しく働いているにもかかわらず、スコープ設計やリソース管理の意識が足りないことで、意図しないバグやパフォーマンス低下につながります。
TypeScriptは型によってある程度の誤用を防げますが、ループ変数のキャプチャとメモリリークに関しては、実行モデルへの理解に依存する部分が大きく、コードの書き方そのものを改善する必要があります。
まず、ループ変数のキャプチャの典型的な落とし穴を整理します。
JavaScriptの世界では、forループの中でクロージャを生成し、そのクロージャを後から呼び出すようなコードを書くと、「すべてのクロージャが同じループ変数を参照してしまう」という挙動に遭遇しがちです。
これは、varによる関数スコープと、クロージャが「定義時のレキシカル環境」を参照するという性質が組み合わさった結果として起きるものです。
問題のあるコード例を見てみます。
function registerHandlers() {
const handlers: Array<() => void> = [];
for (var i = 0; i < 3; i++) {
handlers.push(() => {
console.log("index:", i);
});
}
handlers.forEach(h => h());
}
このコードを実行すると、期待としてはindex: 0, index: 1, index: 2と出力されてほしいところですが、実際にはindex: 3が3回出力されます。
理由は、var iが関数スコープであり、forループのブロックが新しいスコープを作らないためです。
結果として、すべてのクロージャは同じi変数への参照を保持し、その値はループ終了時点で3になっています。
この問題に対する基本的な解決策は、varではなくletを使うことです。
letはブロックスコープを持つため、forループの各イテレーションごとに新しいiのインスタンスが生成され、その値をクロージャが個別に捕まえます。
function registerHandlersSafe() {
const handlers: Array<() => void> = [];
for (let i = 0; i < 3; i++) {
handlers.push(() => {
console.log("index:", i);
});
}
handlers.forEach(h => h());
}
この書き方であれば、期待どおりindex: 0, index: 1, index: 2と出力されます。
TypeScriptのコンパイラ設定によっては、varの使用を避けるようなルールを導入できますが、言語仕様として「letはブロックスコープ」「クロージャは定義時のレキシカル環境を捕まえる」という関係を理解していることが根本的な解決につながります。
もうひとつの対策として、「ループの中で無理にクロージャを生成せず、ループ外で関数ファクトリを使う」という設計もあります。
たとえば次のようなスタイルです。
function createIndexLogger(index: number) {
return () => console.log("index:", index);
}
function registerHandlersFactory() {
const handlers: Array<() => void> = [];
for (let i = 0; i < 3; i++) {
handlers.push(createIndexLogger(i));
}
handlers.forEach(h => h());
}
ここでは、ループ内では単にcreateIndexLoggerを呼び出し、ループごとに異なるindexを持つクロージャを生成しています。
クロージャがどのスコープを捕まえるかを外側の関数で明示的に制御しているため、ループ構造に依存しない形で挙動を説明しやすくなります。
次に、「メモリリーク」の観点でクロージャを考えます。
クロージャは外側のレキシカル環境への参照を保持するため、その環境に含まれているオブジェクトや関数、コールバックなども間接的に参照され続けます。
これ自体は仕様どおりの挙動ですが、不要になったイベントハンドラやタイマー、DOM要素などへの参照をクロージャが持ち続けると、ガベージコレクタがそれらを解放できず、結果としてメモリリークのような状態を生み出します。
メモリリークの典型例として、「DOMイベントハンドラにクロージャを渡しているが、解除を忘れている」ケースを考えてみます。
function attachListeners(element: HTMLElement) {
const largeData = new Array(100000).fill({ /* 重いデータ */ });
const handler = () => {
console.log("clicked", largeData.length);
};
element.addEventListener("click", handler);
}
このコードでは、largeDataという大きな配列がクロージャに閉じ込められています。
handler関数はlargeDataへの参照を持ち続け、elementにイベントハンドラとして登録されています。
もしこのelementが長時間生き続けるUIコンポーネントであり、attachListenersが頻繁に呼ばれる設計になっていると、解除されないイベントハンドラとそれに紐づく大きな配列が積み上がり続ける可能性があります。
これは、クロージャが意図せずにメモリリークの原因となる典型例です。
この問題に対する基本的な対策は、不要になったときにイベントハンドラを明示的に解除することです。
function attachAndDetachListeners(element: HTMLElement) {
const largeData = new Array(100000).fill({ /* 重いデータ */ });
const handler = () => {
console.log("clicked", largeData.length);
};
element.addEventListener("click", handler);
return () => {
element.removeEventListener("click", handler);
};
}
ここでは、イベントハンドラの解除用関数を返しています。
呼び出し側はコンポーネントのライフサイクルに応じてこの解除関数を実行し、クロージャへの参照を外側から切ることができます。
イベントハンドラが登録されている限りは、largeDataを含むレキシカル環境が保持されますが、解除によってその参照がなくなれば、ガベージコレクタが環境と配列を解放できる可能性が高まります。
メモリリークを防ぐうえで意識したいポイントは、「クロージャが保持しているレキシカル環境の中に、寿命の長いリソースを置きすぎない」ということです。
たとえば、巨大なキャッシュや重いオブジェクトをクロージャに閉じ込める場合、そのクロージャ自体のライフタイムがどのくらい長いのかを意識する必要があります。
長く生きるクロージャなら、キャッシュのクリア機構や明示的な破棄関数を用意する方が安全です。
また、ループとメモリリークが組み合わさると、さらに複雑な問題を生むことがあります。
大量の要素に対してイベントハンドラを一括で登録するようなコードを考えてみます。
function attachHandlersToList(items: HTMLElement[]) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
const handler = () => {
console.log("clicked index:", i);
};
item.addEventListener("click", handler);
}
}
このコードでは、各itemにクロージャを紐づけています。
let iを使っているため、ループ変数のキャプチャの問題は避けられていますが、もしitemsが大量であり、後からイベントハンドラを解除しない場合、多数のクロージャがDOM要素にぶら下がり続けることになります。
さらに、クロージャ内部で重いデータ構造を参照している場合、そのぶんだけメモリ消費が増加します。
このような状況で取れる対策としては、次のようなものがあります。
- イベントハンドラを共通化し、必要な情報だけを
data-*属性や別の構造で渡す - コンポーネントのライフサイクルに応じて一括解除するメソッドを用意する
- クロージャ内部から重いデータ構造を直接参照せず、IDやキーだけを持たせて外部の管理層に問い合わせる
TypeScriptの型システムはメモリリーク自体を防ぐことはできませんが、解除用関数や管理用のインターフェースを型として表現することで、「いつどのようにリソースを解放するか」をコード上で明示する助けになります。
最後に、ループ変数のキャプチャとメモリリークに共通する教訓として、「クロージャのライフタイムを常に意識する」という点を強調したいです。
クロージャは強力な抽象ですが、外側の変数環境への参照を持つという性質上、その環境に何を入れるか、そしてどこまで生かし続けるかを意図的に設計しないと、バグやパフォーマンス問題につながります。
TypeScriptでは、letとconstを用いたブロックスコープ設計と、イベントハンドラやキャッシュに対する明示的なクリーンアップ処理を組み合わせることで、ループ変数のキャプチャミスやメモリリークを効果的に防ぐことができます。
イベントハンドラと非同期処理でクロージャを安全に使う設計パターン

クロージャがもっとも頻繁に顔を出す領域のひとつが、イベントハンドラと非同期処理です。
ブラウザのクリックイベント、フォームの入力イベント、Node.jsのストリームイベント、Promiseやタイマーによる非同期ロジックなど、いずれも「後から呼び出される関数」に外側の状態を持たせたい場面が多くなります。
そのとき、クロージャは便利な道具になりますが、スコープ設計を誤ると意図しない状態共有やメモリリーク、予測しにくい副作用につながります。
ここでは、イベントハンドラと非同期処理にフォーカスして、TypeScriptでクロージャを安全に使うための設計パターンを整理していきます。
まず、イベントハンドラとクロージャの基本的な関係から考えます。
DOMのイベントハンドラに関数を登録する場合、その関数はしばしば外側の状態(例えばフォームの検証ルール、APIクライアント、ログ用のタグなど)を参照したいはずです。
ここでクロージャを使うと、イベントが発火したタイミングでその状態を自然に参照できますが、その反面、ハンドラが長く生き続ける間は状態も長く保持されます。
つまり、「何を閉じ込めるか」と「いつ解除するか」を意識した設計が必要になります。
典型的なパターンとして、「設定値と依存オブジェクトをクロージャに閉じ込めたイベントハンドラ」を見てみます。
type ClickConfig = {
logTag: string;
endpoint: string;
};
function createClickHandler(config: ClickConfig, fetcher: (url: string) => Promise<void>) {
return async (event: MouseEvent) => {
console.log(`[${config.logTag}] clicked`, event.target);
await fetcher(config.endpoint);
};
}
function setupButton(button: HTMLButtonElement) {
const handler = createClickHandler(
{ logTag: "MainButton", endpoint: "/clicked" },
async (url) => {
await fetch(url);
}
);
button.addEventListener("click", handler);
return () => button.removeEventListener("click", handler);
}
このコードでは、configとfetcherという依存情報がクロージャに閉じ込められています。
buttonにイベントハンドラを登録し、同時に解除用の関数も返しています。
呼び出し側はコンポーネントのライフサイクルに合わせて解除関数を呼ぶことで、イベントハンドラとそれに紐づくレキシカル環境を明示的に切り離せます。
このように、「登録と解除をセットで設計する」ことが、イベントハンドラにクロージャを使うときの基本パターンになります。
次に、非同期処理との組み合わせを考えます。
Promiseやasync/awaitを使ったコードでは、クロージャは「非同期チェーンの途中で状態を保持する」ために使われます。
特に再試行ロジックやバックオフ戦略、連続したAPI呼び出しの間で状態を共有したいときに、クロージャは有効です。
ただし、「非同期処理の開始タイミング」と「クロージャが捕まえる値のタイミング」が混ざると、意図しない挙動が生じることがあります。
再試行ロジックの例を見てみます。
function createRetrier(maxRetries: number, delayMs: number) {
return async <T>(fn: () => Promise<T>): Promise<T> => {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (e) {
attempt++;
if (attempt > maxRetries) {
throw e;
}
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
};
}
const retrier = createRetrier(3, 1000);
retrier(() => fetch("/api/data").then(res => res.json()));
ここでは、maxRetriesとdelayMsという設定値と、attemptという状態がクロージャに閉じ込められています。
返された関数は、呼び出しごとに独立したattemptカウンタを持ち、最大リトライ回数を超えたら例外を投げます。
このパターンのポイントは、「非同期ループ内部の状態を外側に晒さない」ことです。
呼び出し側はretrierを単なるユーティリティとして扱い、その内部で何回リトライされたかを意識する必要がありません。
このように、非同期処理の制御ロジックをクロージャに閉じ込めることで、関心の分離がしやすくなります。
一方で、非同期処理とイベントハンドラを組み合わせるときは、キャンセルやレースコンディションに注意する必要があります。
例えば、フォーム送信ボタンにAPI呼び出しを紐づける場合、ユーザーが連打したり、コンポーネントがアンマウントされたりする可能性があります。
ここで「ボタンの状態」と「非同期処理の進行状況」をクロージャで共有する設計をとると、安全性を高められます。
function createSubmitHandler(form: HTMLFormElement) {
let inFlight = false;
return async (event: Event) => {
event.preventDefault();
if (inFlight) {
console.log("already submitting");
return;
}
inFlight = true;
try {
const data = new FormData(form);
const res = await fetch("/submit", {
method: "POST",
body: data
});
console.log("submitted", res.status);
} finally {
inFlight = false;
}
};
}
function setupForm(form: HTMLFormElement) {
const handler = createSubmitHandler(form);
form.addEventListener("submit", handler);
return () => form.removeEventListener("submit", handler);
}
この例では、inFlightというフラグがクロージャに閉じ込められています。
これにより、「送信処理が進行中かどうか」という状態をイベントハンドラ間で共有でき、連打による二重送信を防げます。
非同期処理の完了時にinFlightをfalseに戻しているため、状態の一貫性も保たれます。
このように、「非同期処理の進行状態をクロージャで管理する」というパターンは、実務で非常に有用です。
イベントハンドラと非同期処理にクロージャを使うときの設計指針として、次のような点を意識すると安全性が高まります。
- 長期間生きるイベントハンドラに、大きなデータ構造や不要な参照を閉じ込めない
- 登録と解除をセットにしたAPIを設計し、コンポーネントのライフサイクルに沿って必ず解除する
- 非同期処理の進行状態やキャンセルフラグをクロージャに閉じ込め、同時実行やレースコンディションを明示的に制御する
- 設定値や依存オブジェクトはクロージャに閉じ込め、イベントハンドラのシグネチャは
eventを中心にシンプルに保つ
TypeScriptの型システムは、このようなパターンの表現力を高めてくれます。
例えば、解除関数の型を() => voidではなく、() => Promise<void>にすることで、非同期的なクリーンアップ処理が必要な場合にも対応できます。
また、イベントハンドラの型を(event: MouseEvent) => voidのように明示することで、「どのようなイベントに対応しているハンドラなのか」をコード上で表現できます。
さらに一歩踏み込んだ設計として、「イベントハンドラの生成と依存性注入を関数ファクトリにまとめる」というパターンがあります。
これは、クロージャを使ってイベントハンドラの内部に設定値やサービスを閉じ込めると同時に、その構成を1箇所で管理するための構造です。
type Services = {
log: (msg: string) => void;
track: (eventName: string) => void;
};
function createHandlers(services: Services) {
return {
onClick(buttonName: string) {
return (event: MouseEvent) => {
services.log(`clicked: ${buttonName}`);
services.track(`click:${buttonName}`);
};
},
onHover(areaName: string) {
return (event: MouseEvent) => {
services.log(`hover: ${areaName}`);
};
}
};
}
このパターンでは、servicesという依存オブジェクトがクロージャによってcreateHandlersのスコープに閉じ込められています。
onClickやonHoverで生成されるイベントハンドラは、それぞれbuttonNameやareaNameといった追加情報を自分のレキシカル環境に持ちつつ、共通のservicesを利用します。
こうすることで、イベントハンドラのロジックを統一しながら、ボタンごとに異なるタグや名前を柔軟に扱えます。
まとめると、イベントハンドラと非同期処理におけるクロージャの安全な使い方は、「状態を閉じ込める範囲を意識し、ライフタイムとリソース管理を明示的に設計すること」に尽きます。
TypeScriptでは、設定値や依存オブジェクト、非同期処理の進行状態をクロージャに閉じ込めることで、イベントハンドラのシグネチャをシンプルに保ちながら、内側のロジックを柔軟に拡張できます。
その一方で、長生きするハンドラには解除処理を必ず用意し、重いデータやリソースを安易に閉じ込めないようにすることが、メモリリークや予期しない副作用を防ぐうえで重要です。
TypeScriptの型システムでクロージャを強化する:関数ファクトリと依存性注入

これまで見てきたように、クロージャは「関数とレキシカル環境の組み合わせ」として状態や設定値をスコープに閉じ込めるための強力な仕組みです。
ここにTypeScriptの型システムを組み合わせると、単に便利なテクニックにとどまらず、「依存性注入(DI)」や「関数ファクトリ」を通じて設計レベルでの一貫性と安全性を確保する道具になります。
特に、中〜大規模のコードベースでは、クロージャによる状態の隠蔽だけでなく、依存関係の明示とテスト容易性の向上が重要になります。
その意味で、TypeScriptの型システムはクロージャを「強化」する役割を担うと言えます。
まず、「関数ファクトリ」という概念を整理します。
関数ファクトリとは、ある関数(あるいはオブジェクト)を生成する関数のことです。
入力として設定値や依存オブジェクトを受け取り、それらをクロージャに閉じ込めた関数を返します。
この形を取ることで、生成された関数は「必要な依存関係をすでに持っている」状態になります。
一方、TypeScriptの型システムは、関数ファクトリが受け取る依存関係の型と、返す関数の型を明示することで、「何を注入し、何を提供するか」という契約を表現できます。
典型的な例として、サービスロジックに対する依存性注入を考えてみます。
type UserRepository = {
findById(userId: string): Promise<{ id: string; name: string } | null>;
};
type Logger = {
info(message: string): void;
error(message: string): void;
};
type GetUserService = (userId: string) => Promise<string>;
function createGetUserService(repo: UserRepository, logger: Logger): GetUserService {
return async (userId) => {
logger.info(`getUser: ${userId}`);
const user = await repo.findById(userId);
if (!user) {
logger.error(`user not found: ${userId}`);
throw new Error("user not found");
}
return user.name;
};
}
この例では、UserRepositoryとLoggerという2つの依存オブジェクトがcreateGetUserServiceの引数として渡され、クロージャに閉じ込められています。
返されるGetUserService関数は、それら依存オブジェクトを内部で利用しつつ、呼び出し側には(userId: string) => Promise<string>というシンプルなインターフェースだけを提供します。
TypeScriptの型システムは、この構造を次のように明示してくれます。
repoはfindByIdメソッドを持ち、戻り値の型はPromise<{ id: string; name: string } | null>loggerはinfoとerrorメソッドを持ち、それぞれ文字列を受け取るcreateGetUserServiceの返り値はGetUserServiceであり、そのシグネチャは(userId: string) => Promise<string>
このように型で契約を表現することで、「どの依存関係が必要か」「生成された関数は何をするか」をコンパイル時に検証できるようになります。
クロージャによって依存関係がスコープに閉じ込められるため、実行時に追加の引数を渡す必要はありません。
ここで重要なのは、依存性注入をクロージャで行う場合、「引数による注入」と「スコープによる保持」を分離できる点です。
createGetUserServiceは、依存オブジェクトを引数として受け取ることで注入を行い、返された関数はそれらをレキシカル環境として保持します。
一方、呼び出し側から見れば、依存関係はすでに解決済みであり、サービス関数を使うときに意識する必要はありません。
この構造は、テストコードを書くときにも有利です。
const fakeRepo: UserRepository = {
async findById(userId) {
return { id: userId, name: "Fake User" };
}
};
const fakeLogger: Logger = {
info: () => {},
error: () => {}
};
const getUser = createGetUserService(fakeRepo, fakeLogger);
(async () => {
const name = await getUser("test");
console.log(name); // Fake User
})();
ここでは、テスト用のfakeRepoとfakeLoggerを注入しています。
型が一致している限り、実装を自由に差し替えられるため、クロージャによって依存関係が固定されていても、ファクトリ呼び出し時に別のオブジェクトを渡すことで振る舞いを切り替えられます。
依存性注入とクロージャの組み合わせは、「生成時の依存関係解決」と「使用時のインターフェースの単純化」を両立させる点で有効です。
もう少し抽象的なパターンとして、「設定値+サービス」をまとめて注入する関数ファクトリを考えてみます。
type AppConfig = {
apiBaseUrl: string;
};
type HttpClient = {
get<T>(path: string): Promise<T>;
};
type Services = {
config: AppConfig;
http: HttpClient;
};
type FetchUserProfile = (userId: string) => Promise<{ id: string; name: string }>;
function createFetchUserProfile(services: Services): FetchUserProfile {
const { config, http } = services;
return (userId) => {
const path = `/users/${userId}`;
return http.get<{ id: string; name: string }>(config.apiBaseUrl + path);
};
}
この例では、Servicesという依存オブジェクトをまとめた型を用意し、その中に設定値とHTTPクライアントを格納しています。
createFetchUserProfileはservicesを引数として受け取り、クロージャにconfigとhttpを閉じ込めたFetchUserProfile関数を返します。
返り値の関数は、ユーザーIDを受け取ってプロフィールを取得する責務だけを持ち、設定値やHTTPクライアントの具体的な構造には関与しません。
TypeScriptの型システムは、Servicesの変更(例えばhttpのインターフェース拡張)があった場合でも、コンパイル時に影響範囲を特定できます。
ジェネリクスを使うと、このパターンをさらに汎用的にできます。
例えば、「任意のサービス型と任意の設定型を受け取る関数ファクトリ」を設計することも可能です。
type FactoryConfig<TServices, TArgs extends any[], TResult> = {
services: TServices;
impl: (services: TServices, ...args: TArgs) => TResult;
};
function createService<TServices, TArgs extends any[], TResult>(
config: FactoryConfig<TServices, TArgs, TResult>
) {
return (...args: TArgs): TResult => {
return config.impl(config.services, ...args);
};
}
このcreateServiceは、抽象的な関数ファクトリです。
servicesとimpl(具体的な実装)を受け取り、servicesをクロージャに閉じ込めた関数を返します。
TServices、TArgs、TResultというジェネリクスによって、依存関係の型と引数の型、戻り値の型を柔軟に変更できるため、同じパターンをさまざまなサービスで再利用できます。
ここでも、TypeScriptの型システムが「このファクトリはどのような依存関係を受け取り、どのような関数を生成するか」をコンパイル時に明示しています。
依存性注入とクロージャを組み合わせるときに意識したい設計上のポイントを挙げておきます。
- 依存オブジェクトの型を明示し、必要なメソッドだけを含むインターフェースに絞る
- 関数ファクトリの返り値の型を別名(
type)として定義し、サービスの契約を明示する - 設定値とサービスをまとめた型を用意し、注入の単位をわかりやすくする
- ジェネリクスを活用して、再利用可能なファクトリの型パターンを構築する
これらのポイントは、TypeScriptの型システムが「依存関係の構造」を表現する力を持っているからこそ、クロージャと組み合わせる価値があります。
単なるJavaScriptで同じパターンを実装することもできますが、型がないと「何が注入されるべきか」をコードを読まないと把握できず、変更時の影響範囲も見えづらくなります。
TypeScriptでは、依存関係の型定義そのものがドキュメント兼契約として機能します。
もうひとつ重要な観点が、「テスト容易性」です。
依存性注入をクロージャで行う場合、テストコードから見れば「ファクトリに渡す依存オブジェクトを差し替える」だけで、サービス関数の振る舞いを制御できます。
TypeScriptの型が依存オブジェクトの構造を保証してくれるため、モックやスタブの実装も型安全に書けます。
例えば、UserRepositoryのテスト用実装に「特定IDでエラーを返す」などの振る舞いを追加しても、インターフェースに定義されたメソッドの型さえ守っていれば問題ありません。
最後に、TypeScriptの型システムがクロージャを「強化する」とはどういうことかをまとめます。
クロージャ自体はランタイムの機能であり、型には直接現れません。
しかし、
- 依存性注入のために用意したインターフェース
- 関数ファクトリの引数と返り値の型
- ジェネリクスによる抽象化されたサービス定義
といった形で、クロージャが保持するレキシカル環境の構造を間接的に表現できます。
その結果、関数ファクトリと依存性注入を組み合わせた設計は、単に「動けばよいコード」ではなく、「依存関係の構造と契約が型で保証されるコード」へと進化します。
クロージャが持つ柔軟さと、TypeScriptの型システムが持つ厳密さを両立させることで、実務レベルでも扱いやすく、長期的に保守しやすいサービス設計が可能になります。
関数型スタイルで考えるクロージャ:状態管理とテスト容易性を両立する設計

クロージャは「状態を持つ関数」を自然に表現できる仕組みですが、その使い方によっては状態管理が複雑になったり、テストがしにくくなったりします。
そこで有効になるのが、関数型スタイルの考え方です。
関数型スタイルでは、副作用をできるだけ局所化し、データの変換を純粋関数として扱うことで、コードの検証可能性を高めます。
クロージャをこの文脈で捉え直すと、「必要最小限の状態だけをスコープに閉じ込めたテストしやすいコンポーネント」を設計するための道具として位置づけることができます。
まず、関数型スタイルでクロージャを考える際の基本方針を整理します。
関数型スタイルでは、できるだけ純粋関数(同じ入力に対して常に同じ出力を返し、外部状態を書き換えない関数)を増やし、副作用を一箇所に集約することを目指します。
クロージャは「状態を閉じ込める」仕組みなので、一見すると純粋性と相反するように見えます。
しかし、実際には「外側のスコープに置いたミュータブルな状態を直接操作するのではなく、閉じ込めた状態を明示的なインターフェースを通じて制御する」という形で、副作用の範囲を限定することができます。
具体例として、「状態管理ロジックをクロージャに閉じ込めた関数型風ストア」を考えてみます。
type CounterState = {
value: number;
};
type CounterEvent =
| { type: "inc"; amount?: number }
| { type: "dec"; amount?: number }
| { type: "reset" };
type CounterReducer = (state: CounterState, event: CounterEvent) => CounterState;
const counterReducer: CounterReducer = (state, event) => {
switch (event.type) {
case "inc":
return { value: state.value + (event.amount ?? 1) };
case "dec":
return { value: state.value - (event.amount ?? 1) };
case "reset":
return { value: 0 };
default:
return state;
}
};
function createCounterStore(initial: CounterState = { value: 0 }) {
let state = initial;
return {
getState: () => state,
dispatch: (event: CounterEvent) => {
state = counterReducer(state, event);
return state;
}
};
}
ここでは、counterReducerが純粋関数として定義されています。
stateとeventを受け取り、新しいstateを返すだけで、外部の状態に直接アクセスしません。
一方、createCounterStoreはstateをクロージャに保持し、dispatchメソッド内でcounterReducerを通じて状態を更新します。
状態更新という副作用はクロージャ内部に局所化されており、呼び出し側からはdispatchとgetStateという明示的なインターフェースを通じてのみ状態にアクセスできます。
この構造が関数型スタイルと相性が良いのは、純粋な部分と副作用を持つ部分が明確に分割されているからです。
counterReducerはテスト容易性が非常に高く、単体テストでは「ある入力に対して期待される出力を返すか」だけを検証すれば済みます。
クロージャによる状態管理はcreateCounterStoreに集約されており、こちらは「ライフタイム管理」や「API整形」の責務を担います。
このように、クロージャを「純粋関数を包む薄い状態管理層」として位置づけると、関数型スタイルと状態管理を両立しやすくなります。
もう少し実務寄りの例として、「フィルタリングロジックを関数型スタイルで書きつつ、クロージャで設定値を保持する」ケースを考えます。
type Item = {
id: number;
category: string;
price: number;
};
type FilterConfig = {
maxPrice?: number;
category?: string;
};
type FilterFn = (items: Item[]) => Item[];
function createFilter(config: FilterConfig): FilterFn {
const applyPrice = (items: Item[]): Item[] => {
if (config.maxPrice == null) return items;
return items.filter(item => item.price <= config.maxPrice!);
};
const applyCategory = (items: Item[]): Item[] => {
if (!config.category) return items;
return items.filter(item => item.category === config.category);
};
return (items) => {
let result = items;
result = applyPrice(result);
result = applyCategory(result);
return result;
};
}
ここでは、createFilterが関数ファクトリとして設定値をクロージャに閉じ込め、返されたFilterFnは入力の配列を純粋に変換します。
applyPriceやapplyCategoryも純粋関数であり、副作用を持ちません。
関数型スタイルでは、このように「設定値を持つクロージャ」と「データ変換を行う純粋関数」を組み合わせることで、テストがしやすく、再利用性の高いロジックを構築できます。
テストコードの観点から見ると、このパターンのメリットがよくわかります。
counterReducerやapplyPriceなどの純粋関数は、モックやスタブなしで単体テストが書けるcreateCounterStoreやcreateFilterのようなクロージャを使ったファクトリは、「初期状態や設定値を変えたときの振る舞い」をテストで検証できる- クロージャが外側のモジュールスコープやグローバル状態に依存していないため、テスト環境での再現性が高い
関数型スタイルでクロージャを設計する際に重要なのは、「クロージャ内部にどこまでロジックを押し込めるか」です。
状態管理の責務を持つ部分と、状態を入力として受け取って出力を返す純粋な部分を意識的に分離し、クロージャはあくまで「純粋関数を束ねるための小さなコンテナ」として扱うべきです。
さらに、関数型スタイルでは「状態のスナップショット」を重視します。
クロージャを使うと、ある時点の設定値やリソースを閉じ込めた関数を簡単に作れますが、その状態を変える場合には新しいクロージャを生成するという方針を取ると、状態遷移の追跡が容易になります。
type State = {
count: number;
enabled: boolean;
};
type Selector<T> = (state: State) => T;
function createSelectors() {
const selectCount: Selector<number> = (state) => state.count;
const selectEnabled: Selector<boolean> = (state) => state.enabled;
const selectStatus: Selector<string> = (state) =>
state.enabled ? `count: ${state.count}` : "disabled";
return {
selectCount,
selectEnabled,
selectStatus
};
}
この例では、createSelectorsが単に関数群を返しているだけですが、セレクタ自体は純粋関数です。
Stateを入力に取り、特定のビューを返します。
ここでクロージャが活躍するのは、「セレクタ群をまとめたオブジェクト」を生成するときです。
必要に応じて別のState構造に対応したセレクタセットを作ったり、セレクタの構成を差し替えたりできます。
関数型スタイルに従えば、状態の変化はStateの更新で表現し、セレクタはその状態を読む役割に徹します。
クロージャは、そのセレクタ群を構築する段階で設定や依存関係を持たせるための補助的な仕組みになります。
関数型スタイルでクロージャを設計する際に押さえておきたいポイントを整理すると、次のようになります。
- 状態更新ロジックは純粋関数として切り出し、クロージャはその純粋関数を呼び出す役割に限定する
- 設定値や依存オブジェクトをクロージャに閉じ込め、呼び出し側に見せるインターフェースをシンプルに保つ
- 状態の読み取りはセレクタ的な純粋関数で行い、クロージャは読み取り結果の組み合わせを提供する
- クロージャがグローバル状態に依存しないように設計し、テスト環境での再現性を確保する
このような方針に従うと、クロージャは「関数型スタイルの中で、副作用と状態を局所化するための小さな箱」として機能します。
TypeScriptの型システムとも相性が良く、純粋関数の型、状態やイベントの型、ストアやセレクタの型を明示することで、「何がどこで変わるのか」「どのデータがどこから参照されるのか」をコンパイル時に把握しやすくなります。
最後に、関数型スタイルでクロージャを使うことの意義をまとめると、状態管理とテスト容易性を両立できる設計の軸を提供するという点に集約されます。
クロージャは本来、状態を長生きさせる危険な仕組みとしても働き得ますが、関数型の考え方を取り入れることで、その状態を意図的に限定し、純粋関数と組み合わせて透明性の高いロジックに変換できます。
TypeScriptでは、これらの構造を型で表現しながら設計を進められるため、クロージャを「テストしやすい関数型コンポーネント」を作るための実務的な武器として活用できるようになります。
TypeScriptクロージャのベストプラクティス:保守しやすい変数スコープ設計のまとめ

TypeScriptでクロージャを実務的に使いこなすうえで重要なのは、「変数スコープをどう設計するか」を常に意識することです。
クロージャ自体は、関数が定義されたときのレキシカル環境を保持するというシンプルな仕組みですが、その環境に何を入れるか、どこまでライフタイムを伸ばすかによって、コードの保守性は大きく変わります。
ベストプラクティスとしては、局所的な状態を小さなスコープに閉じ込める、不要になった参照は積極的に破棄する、設定値や依存関係は明示的な関数ファクトリを通じて注入する、といった方針を採ることが有効です。
クロージャを直感的に理解するための簡単な例
クロージャの直感的な理解には、シンプルなカウンタやログ関数の例が役に立ちます。
例えば、次のようなコードを考えます。
function greetFactory(prefix: string) {
return (name: string) => `${prefix}, ${name}!`;
}
const hello = greetFactory("Hello");
console.log(hello("TypeScript")); // Hello, TypeScript!
ここでは、prefixが関数スコープに閉じ込められ、返された関数からいつでも参照できます。
このような例から、「関数が定義されたときの外側の変数を覚えている」というクロージャの基本を掴んでおくと、より複雑なケースでも迷いにくくなります。
スコープチェーンとシャドーイングを意識したコード設計
スコープチェーンは、内側のスコープから外側へ順に名前解決を行う仕組みです。
これを意識せずに同じ名前の変数を多用すると、シャドーイングによって「どの値が使われているか」が読みにくくなります。
クロージャが絡むコードでは、特に外側の変数と内側の変数の名前を意図的に分けておくことが重要です。
例えば、configを外側で持つ場合、内側ではlocalConfigやoptionsなど別名を使うことで、どのスコープの値かを一目で判断できるようにしておくと保守性が上がります。
ブロックスコープとvar/let/constの選び方
クロージャを安全に扱うには、varではなくletとconstを使うことが基本です。
varは関数スコープを持つため、ループや条件分岐の内側で宣言してもスコープが分割されず、意図せず外側から参照されることがあります。
一方、letとconstはブロックスコープを持ち、ループごとや条件分岐ごとに独立した変数を生成できます。
特に、非同期処理やイベントハンドラでループ変数をクロージャに閉じ込める場合、letを使うことで「イテレーションごとに別の値を捕まえる」構造を保証できるので安全です。
実行コンテキストスタックとクロージャのライフサイクル
実行コンテキストスタックは、コードがどの関数の中で実行されているかを管理する仕組みであり、その裏側でレキシカル環境が構築されます。
クロージャは、このレキシカル環境への参照を関数オブジェクト内部に保持することで、コンテキストがスタックから外れた後も状態を生かし続けます。
ベストプラクティスとしては、「どのタイミングでクロージャが生成され、どのくらいの期間その環境が生き続けるか」を意識し、長寿命のクロージャに重いデータ構造を閉じ込めないようにすることが重要です。
クリーンなクロージャを保つためのスコープ分割テクニック
クリーンなクロージャを保つためには、スコープの責務を分割するテクニックが有効です。
1つの関数スコープにあまり多くの変数やロジックを詰め込むと、どのクロージャが何に依存しているか分かりにくくなります。
例えば、計算ロジック、設定値、キャッシュなどを別々の関数に切り出し、それぞれをクロージャとして組み合わせることで、依存関係を小さな単位に分解できます。
スコープごとに役割を明確にし、「このスコープには何を閉じ込めるか」を意図的に設計することが、クリーンなクロージャ維持につながります。
カウンタ関数で学ぶミニマルな状態管理
カウンタ関数は、ミニマルな状態管理の良い教材です。
先に示したようなcreateCounterStoreや単純なインクリメント関数を通じて、「状態変数は1つでも良い」「操作用のメソッドは限られた数に抑える」といった設計感覚を養えます。
ミニマルな状態管理を意識すると、クロージャに閉じ込める変数も自然と少なくなり、テストしやすく読みやすいコードになります。
設定値を外出しした関数ファクトリのクロージャパターン
設定値は、関数ファクトリに渡してクロージャに閉じ込めるのが定石です。
HTTPクライアントやロガー、リトライ戦略などの設定を外出しし、その設定を関数ファクトリ経由で注入することで、「設定値の変更」と「ロジックの変更」を分離できます。
これにより、設定変更はファクトリ呼び出し側だけを修正すれば済み、内部実装には影響しません。
TypeScriptでは、設定オブジェクトの型を明示することで、このパターンをより安全に適用できます。
キャッシュをクロージャに閉じ込めることで得られるメリット
キャッシュはクロージャと非常に相性が良い状態です。
クロージャにキャッシュ用の変数を閉じ込めることで、同じ関数呼び出しに対して計算済みの結果を再利用できますし、キャッシュのスコープを特定の関数群に限定できます。
グローバルにキャッシュを置く場合と比べて、どのロジックがどのキャッシュに依存しているかを追いやすくなるため、バグ調査やパフォーマンス調整の際に有利です。
forループと非同期処理で起きる典型的なクロージャバグ
forループと非同期処理を組み合わせたコードでは、「全てのクロージャが同じループ変数を参照する」問題が頻発します。
これは、ループ変数が1つのスコープにまとめられているにもかかわらず、クロージャがその変数を共有してしまうためです。
特にvarを使っている場合に顕著であり、setTimeoutやPromiseの中からループ変数を参照すると、最後の値だけが使われるというバグが生まれます。
letとconstを使った安全なループ変数のキャプチャ方法
この問題に対する基本的な対策が、letとconstを使ったブロックスコープ化です。
for (let i = 0; ...)と書くことで、イテレーションごとに独立したiのインスタンスが生成され、それぞれをクロージャが捕まえます。
さらに、ループ内で別の変数名に代入してからクロージャを生成することで、より明示的に「この値を捕まえている」と説明できるコードになります。
不要な参照を残さないためのクリーンアップ戦略
クロージャはレキシカル環境への参照を保持するため、不要な参照を残し続けるとメモリリークの原因になります。
イベントハンドラやタイマー、キャッシュなどにクロージャを登録した場合、ライフサイクルに応じて確実に解除・破棄する戦略が必要です。
解除用の関数をファクトリから返す、finallyブロックで参照を切るなど、クリーンアップ処理を設計段階で組み込んでおくことがベストプラクティスです。
DOMイベントハンドラの中で状態を持たせるときの注意点
DOMイベントハンドラに状態を持たせる場合、フォームの送信状態やボタンの連打防止フラグなどをクロージャに閉じ込めることがよくあります。
このとき注意したいのは、その状態がどのくらいの期間必要なのか、どの要素と紐づいているのかを明確にすることです。
複数の要素に同じハンドラを使い回す場合、状態を共有するべきか、要素ごとに分けるべきかを事前に設計しておかないと、意図しない挙動を招きます。
Promiseとクロージャを組み合わせた再試行ロジック
再試行ロジックでは、試行回数や遅延時間をクロージャに閉じ込めるパターンが有効です。
Promiseと組み合わせることで、1つの関数呼び出しの中に再試行のルールをカプセル化できます。
これにより、呼び出し側は単に「失敗するかもしれない処理」を渡すだけで済み、再試行の詳細はクロージャ内部に隠蔽されます。
イベント駆動アーキテクチャでのクロージャ活用パターン
イベント駆動アーキテクチャでは、イベントリスナーにクロージャを渡す場面が多数あります。
イベント種別ごとに設定値や依存サービスを閉じ込めたハンドラを生成する関数ファクトリを用意しておくと、イベントの追加・削除に伴う変更範囲を限定できます。
また、リスナー登録と解除のロジックを一箇所にまとめることで、イベントとスコープの関係も把握しやすくなります。
型付きクロージャで依存関係を明示するメリット
TypeScriptの型システムとクロージャを組み合わせると、依存関係をインターフェースとして明示できます。
クロージャが参照する設定値やサービスの型を定義しておくことで、「何が注入されるべきか」「何を提供するか」がコンパイル時に保証されます。
これは、依存性注入やサービスファクトリを設計するうえで大きなメリットです。
DIコンテナとクロージャを組み合わせたTypeScript設計
DIコンテナを使う場合でも、最終的には関数ファクトリを通じてサービスを生成し、クロージャに依存関係を閉じ込める形になります。
コンテナから取得したインスタンスを引数として渡し、返される関数がそれらをレキシカル環境として保持する設計を採ることで、「コンテナ依存の部分」と「ビジネスロジック」を分離できます。
TypeScriptの型によってコンテナの登録内容とサービスの契約を揃えておくと、変更管理が容易になります。
関数型スタイルでの状態管理とクロージャの相性
関数型スタイルでは、純粋関数による状態変換とクロージャによる状態保持を組み合わせることで、透明性の高いロジックを作れます。
状態更新ロジックを純粋関数として切り出し、クロージャはその関数を呼び出す薄いラッパとして設計することで、副作用の範囲を明確にできます。
この相性の良さを意識して設計すると、クロージャを使った状態管理がテストしやすくなります。
テスト容易性を高めるクロージャの切り出し方
テスト容易性を高めるには、クロージャを「小さな責務単位」に切り出すことが重要です。
1つのクロージャに複数の役割を持たせるのではなく、状態管理、設定適用、イベント処理などをそれぞれ別の関数として分割し、それらを組み合わせる構造を取るとテストしやすくなります。
純粋関数部分のテストを優先し、クロージャ自体のテストはライフタイムやインターフェースに関するものに限定するのが効率的です。
オブジェクト指向とクロージャの使い分け戦略
最後に、オブジェクト指向とクロージャの使い分けについて触れておきます。
クラスベースの設計は、継承やポリモーフィズムが必要な場面に向いていますが、単純な状態管理や設定値の注入にはクロージャの方が軽量で扱いやすいことも多いです。
ベストプラクティスとしては、「小さな状態とシンプルなインターフェース」はクロージャで、「複雑なライフサイクルやリッチな振る舞い」はクラスで、といった軸で使い分けると、全体としてバランスの良い設計になります。


コメント