21
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【TypeScript】 this のトリセツ(1.this パラメーター)

Last updated at Posted at 2021-01-07

はじめに

JavaScript でおなじみの this ですが、ご存知の通り、様々な落とし穴があります。
(通常のの 関数と アロー関数で挙動が違う、呼び出し元次第で値が変わる、strict モードか否かで挙動が違う、等々)

TypeScript では、this におけるこれらの落とし穴を避けるために以下の仕組みがあります。

  1. this パラメーター
  2. This に関する Utility Type
  3. 多様性の this の型(Polymorphic this types)

それぞれの仕様をまとめてみました。

まず最初に、 this パラメーターについて説明します。

環境

TypeScript: v4.1.3

コード

Playground Link

前提

本記事は以下の前提で書いています。

モード: Strict モード
tsconfig: `noImplicitThis`が有効

これらの設定について、以下に簡単に記します。

本題からそれるので、不要な方は読み飛ばしてください。

JavaScript における this

最初に、JavaScript における this の扱いについて軽く整理します。

this - JavaScript | MDNの内容を簡単にまとめます。

ほとんどの場合、this の値はどのように関数が呼ばれたかによって決定されます(実行時結合)。これは実行時に割り当てできず、関数が呼び出されるたびに異なる可能性があります。

具体的には、

  1. グローバル実行コンテキスト (すべての関数の外側)
    • 厳格モード(Strict モード)であるかどうかにかかわらず、this はグローバルオブジェクト
  2. 関数コンテキスト
    • 厳格モード: undefined
    • 通常 :グローバルオブジェクト
  3. アロー関数
  4. クラスコンテキスト
    • static メソッド以外をプロトタイプに持つオブジェクト
  5. オブジェクトのメソッド
    • メソッドが呼び出されたオブジェクト(レシーバ)を参照

仕様について詳しく知りたい方は、以下の記事が参考になるかと思います。

また、挙動については、JavaScript の this を理解する多分一番分かりやすい説明が非常に分かりやすかったです。

Strict モード

Strict モードは JavaScript の機能で、これを指定することで JavaScript の挙動の一部を変化させます。

this に限ると、上で触れたように関数内でグローバルオブジェクトを参照できなくなります (undefined となります)。

function fun() {
  console.log(this); // ブラウザだと Window オブジェクト
  return this;
}
console.log(fun() === this); // true
"use strict";
function fun() {
  console.log(this); // undefined
  return this;
}
console.log(fun() === undefined); // true

TypeScript では、strict or alwaysStrict オプションを使用している場合は、常に Strict モード扱いとなります1

以後、Strict モードである前提で話を進めます。

noImplicitThis

Raise error on ‘this’ expressions with an implied ‘any’ type.

thisの型が暗黙的にanyになる場合、エラーが出るようになります。
参考: TSConfig Reference - Docs on every TSConfig - TypeScript

以下のような関数の場合、this は実行されるコンテキストによって値が異なります。
こういった場合に、noImplicitThisを有効にしていると、エラーが出力されます。

function fn() {
  // 'this' implicitly has type 'any' because it does not have a type annotation.ts(2683)
  console.log(this);
}
fn(); // undefined

const obj = { fn, param: 1 };
obj.fn(); // { "param": 1 }

詳細な挙動については、こちらの記事が参考になります。

さて、本題です。

this parameter

以下のような関数の場合、obj.fn()とした場合は正しく name が表示されます。
しかし、呼び出し元が変わると正しく表示されなくなります。

const obj = {
  name: "foo",
  fn() {
    console.log(this.name);
  },
};
obj.fn(); // "foo"
const fn = obj.fn;

fn(); // TypeError: Cannot read property 'name' of undefined

const obj2 = { fn: obj.fn };
obj2.fn(); // undefined

これを避けるために、関数の第一引数に this の型を指定することができます。
this の型を指定することで、実行時のコンテキストの this が指定した型と異なる場合、エラーが出力されるようになります。

const obj = {
  name: "foo",
  fn(this: { name: string }) {
    console.log(this.name);
  },
};
obj.fn(); // "foo"
const fn = obj.fn;
// The 'this' context of type 'void' is not assignable to method's 'this' of type '{ name: string; }'.ts(2684)
fn();

const obj2 = { fn: obj.fn };
// The 'this' context of type '{ fn: (this: { name: string; }) => void; }' is not assignable to method's 'this' of type '{ name: string; }'.
// Property 'name' is missing in type '{ fn: (this: { name: string; }) => void; }' but required in type '{ name: string; }'.ts(2684)
obj2.fn();

const obj3 = {
  name: "bar",
  address: "fuga", // 余分なプロパティがあってもOK
  fn: obj.fn,
};

// obj3にはname プロパティが存在するため、OK
obj3.fn(); // "bar"
class Cls {
  name = "foo";
  fn(this: Cls) {
    console.log(this.name);
  }
}
const cls = new Cls();
cls.fn(); // foo

const fn = cls.fn;
fn(); // The 'this' context of type 'void' is not assignable to method's 'this' of type 'Cls'.ts(2684)

尚、この第一引数の this は js にトランスパイル後は表示されません。

// js
"use strict";
const obj = {
  name: "foo",
  fn() {
    console.log(this.name);
  },
};

そのため、引数を指定したい場合は、第二引数以降に指定します。

const obj = {
  name: "foo",
  fn(this: { name: string }, age: number) {
    console.log(this.name);
    console.log(age);
  },
};

const obj4 = {
  name: "bar",
  fn: obj.fn,
};
// 呼び出す側で第一引数に指定したものが、fnの第二引数に渡される
obj4.fn(10); // "bar", 10

コールバック内の this パラメーター

コールバック内で this を呼び出すと、呼び出し元が異なるために実行時にエラーが発生しやすいです。

const fn = (callback: () => void) => callback();
class Cls {
  name = "foo";
  fn(this: Cls) {
    console.log(this.name);
  }
}
const cls = new Cls();
fn(cls.fn); // TypeError: Cannot read property 'name' of undefined

対策として、コールバックに{this: void}を指定する方法があります。
こうすると、fnの求める this(void)と、コールバックに渡した関数cls.fnの this(Cls)が異なるため、型エラーになります。

const fn = (callback: (this: void) => void) => callback();
class Cls {
  name = "foo";
  fn(this: Cls) {
    console.log(this.name);
  }
}
const cls = new Cls();
// Argument of type '(this: Cls) => void' is not assignable to parameter of type '(this: void) => void'.
//  The 'this' types of each signature are incompatible.
//   Type 'void' is not assignable to type 'Cls'.ts(2345)
fn(cls.fn);

もし、コールバック内で this を使いたい場合、アロー関数を使う必要があります。

const fn = (callback: () => void) => callback();
class Cls {
  name = "foo";
  fn() {}
  arrow = () => {
    console.log(this.name);
  };
}
const cls = new Cls();
fn(cls.arrow); // "foo"

しかし、アロー関数はプロパティと同様に prototype に割り当てられることに留意する必要があります。
一方メソッドは一度だけ作成され、 Cls オブジェクト全体で共有されます。

// js
class Cls {
  constructor() {
    this.name = "foo";
    this.arrow = () => {
      console.log(this.name);
    };
  }
  fn() {}
}

参考: this パラメーター | TypeScript 日本語ハンドブック | js STUDIO

参考文献

  1. tsc CLI Options

21
14
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
21
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?