はじめに
現在、TypeScriptを勉強しながら業務でもコードを書いているのですが、アロー関数の this
があまりよくわかっていませんでした。また、コンパイラオプションに noImplicitThis
というものがあることも知り、this
にまつわる潜在的なバグを防ぐ仕組みに興味を持ちました。
そこで、今回はアロー関数の this
と noImplicitThis
について調べたことを、具体的なコード例を交えながらまとめてみました。
function
と this
まず、アロー関数がなぜ便利なのかを理解するために、従来の function
で定義された関数における this
の挙動を知る必要があります。
function
における this
は、「どのように呼び出されたか」によって動的に決まります。これが混乱の元凶です...。
例えば、object.method()
のようにメソッドとして呼び出された場合、this
はその object
を指します。一方、関数をそのまま myFunction()
のように呼び出したい場合、this
はグローバルオブジェクト(window
など)を指します。setTimeout
のコールバックはこの後者のパターンに近いため、問題が起こります。
具体例
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
// この時点では、thisはGreeterのインスタンスを指している。
console.log("greet内:", this.greeting);
setTimeout(function() {
// しかし、このfunctionの中では`this`が変わってしまう。
// 呼び出し元がsetTimeout(グローバルな関数)なので、
// `this`はwindowオブジェクト(またはstrict modeではundefined)を指す。
console.log("setTimeout内:", this.greeting); // undefined (エラーの原因)
}, 1000);
}
}
const g = new Greeter("Hello, world!");
g.greet();
この例では、set.Timeout
のコールバック関数内では this
が Greeter
インスタンスを指してくれないため、this.greeting
が undefined
となり、意図しない挙動になってしまいます。
これを回避するために、 const self = this;
のように this
を別の変数に対比させたり、.bind(this)
を使って this
を束縛したりといった方法が昔はよく使われていました。
アロー関数の this
この this
の問題をエレガントに解決してくれるのが、アロー関数です。
アロー関数の最大の特徴は、自身の this
を持たないことです。アロー関数の中で this
を参照すると、それはアロー関数が定義された場所の this
をそのまま参照します。
先ほどの例をアロー関数で書き直してみます。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
console.log("greet内:", this.greeting);
// アロー関数を使用
setTimeout(() => {
// この中の`this`は、アロー関数が定義された場所、
// つまりgreetメソッドの`this`(Greeterのインスタンス)を指す。
console.log("setTimeout内:", this.greeting);
}, 1000);
}
}
const g = new Greeter("Hello, world!");
g.greet();
このように、アロー関数を使うだけで self
や .bind()
を使わずに、自然に意図した this
を扱えるようになります。
コンパイラオプションの noImplicitThis
でバグを未然に防ぐ
TypeScriptには、this
にまつわるバグを未然に防いでくれる noImplicitThis
というコンパイラオプションがあります。
tsconfig.json
でこのオプションを true
にすると、TypeScriptが「この this
って型注釈がなくて文脈からも型が推論できないから、暗黙的に any
型になるけど大丈夫そ?」と教えてくれるわけです。
具体的なカウンターボタンの実装例で見てみましょう。
具体的なシナリオ:カウンターボタン
まず、このようなHTMLがあるとします。
<button id="counter-button">Click me!</button>
<p>Count: <span id="display">0</span></p>
このボタンを操作するクラスを考えます。
問題が起きるコード (function
を使用)
イベントリスナーのコールバックを function
で書くと、noImplicitThis
はエラーを報告します。
// "noImplicitThis": true の場合
class Counter {
private count = 0;
private button: HTMLButtonElement;
private display: HTMLElement;
constructor() {
this.button = document.getElementById('counter-button') as HTMLButtonElement;
this.display = document.getElementById('display') as HTMLElement;
// イベントリスナーを設定
this.button.addEventListener('click', function() {
// ▼ コンパイルエラー ▼
// 'this' implicitly has type 'any' because it does not have a type annotation.
this.increment();
});
}
private increment() {
this.count++;
this.display.textContent = this.count.toString();
}
}
addEventListener
のコールバック内では、this
は Counter
インスタンスではなく、クリックされた button
要素を指します。button
要素に increment
メソッドはないため、TypeScriptが「この this
って型注釈がないから、暗黙的に any
型になるけど大丈夫そ?」と教えてくれるわけです。
解決策 (アロー関数を使用)
このエラーも、コールバックをアロー関数にするだけで解決します。
class Counter {
private count = 0;
private button: HTMLButtonElement;
private display: HTMLElement;
constructor() {
this.button = document.getElementById('counter-button') as HTMLButtonElement;
this.display = document.getElementById('display') as HTMLElement;
// アロー関数でイベントリスナーを設定
this.button.addEventListener('click', () => {
// ✅ `this`はCounterインスタンスを指すため、エラーにならない!
this.increment();
});
}
private increment() {
this.count++;
this.display.textContent = this.count.toString();
}
}
new Counter();
アロー関数内の this
は、外側の constructor
の this
、つまり Counter
インスタンスを指してくれるため、安全にメソッドを呼び出すことができます。
まとめ
今回 this
について調べてみて、アロー関数がなぜ便利なのか、その背景にある function
の this
の挙動から理解することができました。
-
function
のthis
は、呼び出され方によって変わり、混乱の原因になりやすい。 -
アロー関数の
this
は、書かれた場所のthis
を引き継ぐため、直感的で安全。 -
noImplicitThis
を有効にすると、this
にまつわる潜在的なバグをコンパイル時に発見できる。
この3点を押さえておくだけで、今後のTypeScriptでの開発がよりスムーズになりそうだと感じました。noImplicitThis
は、プロジェクトの初期設定で必ず true
にしておきたいですね。
ちなみに、tsconfig.json
で "strict": true
を設定すると、この noImplicitThis
を含む一連の厳格な型チェックがすべて有効になります。新しいプロジェクトを開始する際は、設定しておくのがいいのかもしれません。
おわり。