まず「this を型として扱う」という発想から
JavaScript ではおなじみの this ですが、
TypeScript では 「this そのものにも型を付けられる」 というのがポイントです。
function f() {
console.log(this);
}
TypeScriptこのままだと this は any に近い扱いになりがちで、
「ここでの this は何なのか?」がコードから読み取りにくくなります。
TypeScript では、
function f(this: SomeType, arg: number) { ... }
TypeScriptのように、「最初の引数として this の型を宣言する」 ことで、
「この関数は、こういう this を前提に書かれている」というのを
コンパイラに伝えることができます。
基本形:this パラメータで「この関数は何にバインドされるか」を宣言する
this パラメータの書き方
this の型を明示する基本形はこうです。
function greet(this: { name: string }) {
console.log(`Hello, ${this.name}`);
}
TypeScriptここでのポイントは、
関数の「一番最初の引数」に this: 型 を書く
この this は「実際の引数としては渡されない」
あくまで「この関数の中で使われる this の型」を宣言しているだけ
ということです。
呼び出し側は、call / apply / bind などで this を指定します。
const person = { name: "Taro" };
greet.call(person); // Hello, Taro
TypeScriptこのとき TypeScript は、
greet は「this が { name: string } であることを前提にしている」greet.call(person) の person がその型を満たしているか
をチェックしてくれます。
this を使う関数の中での型安全
さっきの greet の中で this.name にアクセスするとき、this の型が { name: string } なので、name プロパティが必ず存在すると分かります。
function greet(this: { name: string }) {
// this: { name: string }
console.log(this.name.toUpperCase()); // OK
}
TypeScriptもし name がないオブジェクトを this に渡そうとすると、コンパイルエラーになります。
const obj = { title: "Book" };
// greet.call(obj); // エラー: '{ title: string }' に 'name' がない
TypeScriptここが 「this に型を付ける一番おいしいところ」 です。
「this を前提に書いた関数」が、間違った this で呼ばれるのを防げます。
メソッドと this の型:オブジェクトリテラルでの this
オブジェクトのメソッドで this を型付けする
オブジェクトリテラルのメソッドでも、this パラメータを使えます。
const counter = {
value: 0,
increment(this: { value: number }) {
this.value++;
},
};
counter.increment();
TypeScriptここでは、
increment の中の this の型は { value: number }counter は { value: number; increment: ... }
なので、counter.increment() のときに this は { value: number } として扱われます。
この書き方の良いところは、
「このメソッドは、this に value: number があることを前提にしている」
というのが、型としてはっきり見えることです。
this を「そのオブジェクト自身」にしたい場合
もう少しちゃんと書くなら、オブジェクト全体の型を使うこともできます。
type Counter = {
value: number;
increment(this: Counter): void;
};
const counter: Counter = {
value: 0,
increment() {
this.value++;
},
};
TypeScriptここでは、
increment(this: Counter) と宣言しておくことで、increment の中の this は常に Counter 型として扱われます。
この設計のメリットは、
メソッドの中で this.value 以外にプロパティを増やしても、型チェックが効くCounter を実装する別のオブジェクトでも、同じ this 前提で書ける
というところです。
クラスと this:基本的には「クラスのインスタンス型」
クラスのメソッドでは this 型は自動で推論される
クラスの場合、通常は this の型を明示しなくても、
「そのクラスのインスタンス型」として扱われます。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}`);
}
}
TypeScriptgreet の中の this は Person 型です。
const p = new Person("Taro");
p.greet(); // Hello, Taro
TypeScriptこのレベルでは、特に this の型を明示する必要はありません。
クラスでも this パラメータを明示したくなる場面
例えば、「このメソッドはサブクラスでも this をそのまま使いたい」というときに、this 型(特殊な型)を使うことがあります。
class Builder {
value = 0;
add(n: number): this {
this.value += n;
return this;
}
}
class AdvancedBuilder extends Builder {
multiply(n: number): this {
this.value *= n;
return this;
}
}
const b = new AdvancedBuilder();
b.add(1).multiply(2).add(3);
TypeScriptここでの add の戻り値型 this は、
「呼び出したインスタンスの型そのもの」を表します。
AdvancedBuilder から add を呼ぶと、戻り値の型も AdvancedBuilder になるので、
そのまま multiply をチェーンできます。
これは「this を戻り値の型として使う」パターンですが、
「this を型として扱う」という意味では同じ発想です。
this を使わない関数に「this: void」を付ける意味
「この関数は this を使いません」という宣言
TypeScript には、こんな書き方もあります。
function fn(this: void, x: number) {
console.log(x);
}
TypeScriptthis: void と書くと、
「この関数は this を前提にしていない」
「this を使ってはいけない」
という意味になります。
これを call などで変な this で呼ぼうとすると、エラーになります。
const obj = { value: 1 };
// fn.call(obj, 123); // エラー: this は void でなければならない
fn(123); // OK
TypeScriptここでの重要ポイントは、
「this を使わない関数には、あえて this: void を付けておくと、“this に依存しない純粋な関数”であることを型で保証できる」
ということです。
「this に依存しない設計」を徹底したいときに、this: void はかなり強力な宣言になります。
this を使う関数とアロー関数:決定的な違い
アロー関数には「this パラメータを書けない」
ここはかなり重要なポイントです。
const obj = {
value: 0,
// これは OK(普通のメソッド)
inc(this: { value: number }) {
this.value++;
},
// これは NG(アロー関数には this パラメータを書けない)
// incArrow: (this: { value: number }) => {
// this.value++;
// },
};
TypeScriptアロー関数は「自分自身の this を持たず、外側の this をキャプチャする」仕様なので、
TypeScript でも this: 型 のような宣言はできません。
つまり、
「this に依存する関数を設計したいなら、アロー関数ではなく普通の function / メソッド構文を使う」
というのが鉄則になります。
逆に言うと、
「this に依存させたくない関数」はアロー関数にしておくと、this をうっかり使うことがなくなります。
this を使うかどうかで、
普通の function / メソッド構文にするか
アロー関数にするか
を意識的に選ぶと、設計がかなりスッキリします。
まとめ:this を使う関数の型指定を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
TypeScript では、
関数の「最初の引数」として this: 型 を書くことで、
「この関数は、こういう this を前提にしている」と宣言できる。
これによって、
this を使う関数の中で、プロパティアクセスが型安全になる
間違った this で呼び出すとコンパイルエラーになる
「this を使わない関数」は this: void で“純粋さ”を宣言できる
クラスのメソッドでは this は自動でクラス型になるが、
オブジェクトリテラルや自由関数では this パラメータを明示すると意図がはっきりする。
そして、
「this に依存する関数」は普通の function / メソッド構文で書き、
「this に依存させたくない関数」はアロー関数+this: void という設計にすると、
コードの意図と型がきれいに揃っていきます。
