はじめに
現在、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 を含む一連の厳格な型チェックがすべて有効になります。新しいプロジェクトを開始する際は、設定しておくのがいいのかもしれません。
おわり。