はじめに
JavaScript でおなじみの this ですが、ご存知の通り、様々な落とし穴があります。
(通常のの 関数と アロー関数で挙動が違う、呼び出し元次第で値が変わる、strict モードか否かで挙動が違う、等々)
TypeScript では、this におけるこれらの落とし穴を避けるために以下の仕組みがあります。
それぞれの仕様をまとめてみました。
まず最初に、 this パラメーターについて説明します。
環境
TypeScript: v4.1.3
コード
前提
本記事は以下の前提で書いています。
モード: Strict モード
tsconfig: `noImplicitThis`が有効
これらの設定について、以下に簡単に記します。
本題からそれるので、不要な方は読み飛ばしてください。
JavaScript における this
最初に、JavaScript における this の扱いについて軽く整理します。
this - JavaScript | MDNの内容を簡単にまとめます。
ほとんどの場合、this の値はどのように関数が呼ばれたかによって決定されます(実行時結合)。これは実行時に割り当てできず、関数が呼び出されるたびに異なる可能性があります。
具体的には、
- グローバル実行コンテキスト (すべての関数の外側)
- 厳格モード(Strict モード)であるかどうかにかかわらず、this はグローバルオブジェクト
- 関数コンテキスト
- 厳格モード: undefined
- 通常 :グローバルオブジェクト
- アロー関数
- レキシカル(静的)コンテキストの this を参照
- call, bind, apply で渡された this を無視する
- クラスコンテキスト
- static メソッド以外をプロトタイプに持つオブジェクト
- オブジェクトのメソッド
- メソッドが呼び出されたオブジェクト(レシーバ)を参照
仕様について詳しく知りたい方は、以下の記事が参考になるかと思います。
- this - JavaScript | MDN
- アロー関数 - JavaScript | MDN
- Understanding JavaScript Function Invocation and "this"
また、挙動については、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
参考文献
- プログラミング TypeScript ――スケールする JavaScript アプリケーション開発
- this パラメータ | TypeScript 日本語ハンドブック | js STUDIO
- 多様性の this の型(Polymorphic this types) | TypeScript 日本語ハンドブック | js STUDIO
- ThisParameterType | Documentation - Utility Types - TypeScript
- TSConfig Reference - Docs on every TSConfig - TypeScript