2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

現在、TypeScriptを勉強しながら業務でもコードを書いているのですが、アロー関数の this があまりよくわかっていませんでした。また、コンパイラオプションに noImplicitThis というものがあることも知り、this にまつわる潜在的なバグを防ぐ仕組みに興味を持ちました。

そこで、今回はアロー関数の thisnoImplicitThis について調べたことを、具体的なコード例を交えながらまとめてみました。

functionthis

まず、アロー関数がなぜ便利なのかを理解するために、従来の 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 のコールバック関数内では thisGreeter インスタンスを指してくれないため、this.greetingundefined となり、意図しない挙動になってしまいます。

これを回避するために、 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 のコールバック内では、thisCounter インスタンスではなく、クリックされた 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 は、外側の constructorthis、つまり Counter インスタンスを指してくれるため、安全にメソッドを呼び出すことができます。

まとめ

今回 this について調べてみて、アロー関数がなぜ便利なのか、その背景にある functionthis の挙動から理解することができました。

  • functionthis は、呼び出され方によって変わり、混乱の原因になりやすい。
  • アロー関数の this は、書かれた場所の this を引き継ぐため、直感的で安全。
  • noImplicitThis を有効にすると、this にまつわる潜在的なバグをコンパイル時に発見できる。

この3点を押さえておくだけで、今後のTypeScriptでの開発がよりスムーズになりそうだと感じました。noImplicitThisは、プロジェクトの初期設定で必ず true にしておきたいですね。

ちなみに、tsconfig.json"strict": true を設定すると、この noImplicitThis を含む一連の厳格な型チェックがすべて有効になります。新しいプロジェクトを開始する際は、設定しておくのがいいのかもしれません。

おわり。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?