はじめに (対象読者・この記事でわかること)
この記事は、TypeScript でクラスを利用している開発者、特に「メンバー変数がメソッド内部やコールバック内で undefined になる」現象に直面したことがある方を対象としています。
本稿を読むことで、this のバインドが期待通りに機能しない原因を理解し、アロー関数や bind、型定義の見直しといった具体的な対策を実装できるようになります。実際のエラー例とその解決過程をコード付きで示すので、同様の問題に遭遇したときに即座に対処できる実践的スキルが身につきます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- TypeScript の基本的な文法(型注釈・クラス構文)
- JavaScript の this の概念とスコープ
メンバー変数がローカルで参照できない原因と基本概念
TypeScript は JavaScript の上に型安全性を付与した言語です。そのため、実行時の挙動は JavaScript と完全に同一です。
クラス内で定義したメンバー変数(プロパティ)は、インスタンス化されたオブジェクトの一部として this 経由で参照されます。ところが、次のようなケースで this が期待したオブジェクトを指さないことがあります。
-
普通の関数としてコールバックに渡したとき
ts class Counter { private count = 0; start() { setInterval(function () { console.log(this.count); // this が Window/undefined になる }, 1000); } }setIntervalに渡したコールバックは普通の関数として実行されるため、thisの参照先はグローバルオブジェクト(strict モードではundefined)になります。 -
メソッドを別の変数に代入したとき
ts const inc = counter.increment; inc(); // this が失われるメソッド参照をそのまま代入すると、実行時にthisがバインドされなくなります。 -
型定義が不十分なとき
privateやprotectedキーワードは TypeScript のコンパイル時チェックにのみ影響し、実行時のスコープには関与しません。そのため、型エラーが出ていなくても実際にはundefinedが返るケースがあります。
これらの問題は「スコープのずれ」や「this バインドの失敗」と総称され、主に 関数の呼び出しコンテキスト が期待通りでないことが根本原因です。対策としては、this を固定する手段(アロー関数、bind、変数への代入)や、型情報を正しく整備することが必要です。
実際のコード例と対処手順
以下では、実務でよく遭遇するパターンを踏まえて、段階的に問題を特定し、解決する手順を示します。
ステップ1 – 現象の再現とエラーメッセージの確認
Tsclass Logger { private prefix = '[LOG]'; log(message: string) { setTimeout(function () { // ここで this.prefix が undefined になる console.log(this.prefix, message); }, 500); } } const logger = new Logger(); logger.log('Hello, world!');
実行するとコンソールは undefined Hello, world! と表示されます。TypeScript のコンパイルはエラーにならないため、実行時エラーとして認識しにくい点がポイントです。
ステップ2 – this バインドの確認方法
this がどこを指すかを確かめる最も手軽な方法は、デバッグ時に console.log(this) を出すことです。上記コードに以下を追記すると、Window(あるいは undefined)が出力されます。
TssetTimeout(function () { console.log('this は何か?', this); console.log(this.prefix, message); }, 500);
ステップ3 – アロー関数で this をレキシカルに固定
アロー関数は定義されたスコープの this をレキシカルに捕捉します。最もシンプルな修正は、コールバックをアロー関数に置き換えることです。
Tslog(message: string) { setTimeout(() => { console.log(this.prefix, message); // 正しく [LOG] が表示される }, 500); }
この変更だけで、this が Logger インスタンスを指すようになり、期待通りの出力が得られます。
ステップ4 – bind を使って明示的に this を固定
アロー関数が使えないケース(例えば、外部ライブラリが普通の関数を要求する場合)では、Function.prototype.bind を活用します。
Tslog(message: string) { setTimeout(function () { console.log(this.prefix, message); }.bind(this), 500); }
bind(this) によって、コールバック実行時に this が Logger インスタンスに固定されます。
ステップ5 – メソッド参照の失敗を防ぐパターン
メソッドを別の変数に代入して呼び出すときは、必ず this をバインドした形で渡すようにします。
Tsclass Counter { private count = 0; increment() { this.count++; console.log(this.count); } } const counter = new Counter(); const inc = counter.increment.bind(counter); inc(); // 正しく 1 が表示される
ハマった点やエラー解決
| 現象 | 原因 | 解決策 |
|---|---|---|
this が undefined になる |
コールバックが普通関数として呼び出された | アロー関数に置き換える、または bind で明示的にバインド |
コンパイルは通るが実行時に undefined が出る |
TypeScript の型チェックが this の実行時バインドを保証しない |
デバッグで this を確認し、必要に応じてコードをリファクタリング |
| メソッドをイベントハンドラに渡すとエラーになる | イベントリスナが this を DOM 要素にバインドする |
addEventListener('click', this.handleClick.bind(this)) のようにバインド |
解決策の総括
- アロー関数は最も簡潔で安全な
this固定手段です。可能な限りコールバックはアロー関数で記述しましょう。 bindはレガシーコードや外部 API が普通関数を要求する場合の代替手段です。バインド漏れがないよう注意が必要です。- 型定義の見直しは、IDE の補完やコンパイルエラーに頼らず、実行時の挙動を意識した設計を促します。
privateやprotectedのみで安全は保証されません。
まとめ
本記事では、TypeScript のクラスでメンバー変数がローカルスコープから参照できなくなる典型的なケースと、this バインドの失敗が根本原因であることを解説しました。
- アロー関数でレキシカルな this を取得
- bind による明示的バインド
- メソッド参照時のバインド忘れ防止
これらの対策を習得すれば、実行時に突然 undefined が返る不具合を素早く特定し、安定した TypeScript コードを書けるようになります。次回は、this のバインドを自動化するデコレータパターンについて詳しく解説する予定です。
参考資料
- TypeScript Handbook – Classes
- MDN Web Docs – Arrow functions
- 「Effective TypeScript」 – マシュー・トラヴァッティ 著、O'Reilly Japan