TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – this を使う関数の型指定

TypeScript
スポンサーリンク

まず「this を型として扱う」という発想から

JavaScript ではおなじみの this ですが、
TypeScript では 「this そのものにも型を付けられる」 というのがポイントです。

function f() {
  console.log(this);
}
TypeScript

このままだと thisany に近い扱いになりがちで、
「ここでの 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}`);
  }
}
TypeScript

greet の中の 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);
}
TypeScript

this: 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 という設計にすると、
コードの意図と型がきれいに揃っていきます。

タイトルとURLをコピーしました