1. ホーム
  2. TypeScript

【TypeScript】this パラメータの使い方|関数の中の this に型を付ける方法

Share

JavaScript の this は「どう書いたか」ではなく「どう呼ばれたか」で中身が変わります。そのため、関数の中で this.name と書いても TypeScript はそれが何なのか分からず、補完も効かなければ間違いにも気づけません。これを解決するのが this パラメータです。この記事では、this パラメータの書き方、addEventListener やオブジェクトのメソッドでの実践例、noImplicitThis による検出、ThisParameterType / OmitThisParameter といった組み込み型、そしてエラーが出たときの対処までを解説します。

this の型が分からないと何が困るのか

function で定義した関数の this は、呼び出し方によって変わります。obj.method() と呼べば obj になり、変数に代入してから fn() と呼べば undefined(strict モード)になり、fn.call(x) と呼べば x になります。関数の定義だけを見ても正体が決まらないわけです。

その結果、型の恩恵がまったく受けられません。this.name と書いてもエディタは候補を出してくれませんし、存在しないプロパティを触っていてもコンパイルは通ってしまい、実行時に undefined を読んで初めて気づく、ということが起こります。this パラメータは、この「呼ばれ方まかせの this」に対して、この関数はこういう this で呼ばれる前提ですと宣言するための仕組みです。

this パラメータの基本の書き方

関数の第1引数の位置に、this: 型 と書きます。名前は必ず this でなければならず、位置も必ず先頭です。これは実際の引数ではない特別な仮引数(フェイクパラメータ)で、呼び出すときに値を渡すことはありませんし、JavaScript にコンパイルされた時点できれいに消えます。

index.ts
// 第1引数の位置に this の型を宣言する
function greet(this: { name: string }, greeting: string): string {
  // this に補完が効き、typo もエラーになる
  return `${greeting}, ${this.name}`;
}

const user = { name: 'yamada', greet };

// 呼び出し側は this を渡さない。実引数は greeting だけ
console.log(user.greet('Hello')); // Hello, yamada

// this が付いてこない呼び方はエラーになる
const detached = user.greet;
detached('Hello');
// エラー: The 'this' context of type 'void' is not assignable to
//        method's 'this' of type '{ name: string; }'.

ポイントは、引数の数が増えたわけではないことです。greet の実際の引数は greeting の1つだけで、user.greet('Hello') と呼びます。コンパイル結果を見ると、this パラメータが存在しないことがはっきり分かります。

index.js(コンパイル後)
// this: { name: string } は跡形もなく消える
function greet(greeting) {
  return `${greeting}, ${this.name}`;
}

つまり this パラメータは、実行時の動作を1ミリも変えない「型のためだけの注釈」です。安全に後付けできるので、既存のコードに少しずつ足していくこともできます。

addEventListener のコールバックで this に型を付ける

this パラメータがもっとも役立つのが DOM のイベントハンドラです。addEventListenerfunction 式を渡すと、その中の this はイベントが登録された要素になります。event.currentTargetEventTarget | null という広い型で返ってくるのに対し、this なら要素の型そのものとして扱えるのが利点です。

button.ts
const button = document.querySelector('button');

// その場に書いた function 式なら、this は HTMLButtonElement と推論される
button?.addEventListener('click', function () {
  this.disabled = true; // 補完が効く
});

// 別の場所で定義した関数は文脈から推論されないので、自分で this を書く
function disableSelf(this: HTMLButtonElement, event: MouseEvent): void {
  this.disabled = true;
  this.textContent = '送信中…';
  console.log(event.type); // click
}

button?.addEventListener('click', disableSelf);

addEventListener のリスナー引数は、標準の型定義(lib.dom.d.ts)の中ですでに (this: HTMLButtonElement, ev: MouseEvent) => any のように this パラメータ付きで宣言されています。そのため、その場で書いた無名関数には型が流れ込みますが、独立した関数として定義したものには流れ込みません。後者に this パラメータを書いておけば、間違った要素に登録しようとした時点でエラーにできるという副次的なメリットもあります。

なお、この用途でアロー関数を使うと this は要素ではなく外側のスコープの this になってしまいます。要素を this で受け取りたい場面では function 式を選んでください。

オブジェクトのメソッドで this の型を明示する

インターフェースでメソッドを定義するとき、this パラメータを書いておくと「そのオブジェクトから呼ばれること」を型のレベルで要求できます。メソッドを変数に取り出して this を失ったまま呼ぶ、という定番のバグをコンパイル時に止められます。

counter.ts
interface Counter {
  count: number;
  increment(this: Counter): void;
}

const counter: Counter = {
  count: 0,
  increment() {
    this.count += 1; // this は Counter として扱える
  },
};

counter.increment();
console.log(counter.count); // 1

// メソッドだけ取り出すと this が外れるのでエラーになる
const increment = counter.increment;
increment();
// エラー: The 'this' context of type 'void' is not assignable to
//        method's 'this' of type 'Counter'.

// bind すれば this を固定できるので安全に渡せる
setTimeout(counter.increment.bind(counter), 1000);

クラスのメソッドの場合、this は既定でそのクラスのインスタンス型になるため、通常は this パラメータを書く必要はありません。ただし「特定の条件を満たしたインスタンスからしか呼べない」ことを表したいときなど、既定より狭い型を要求する目的で明示することはあります。

noImplicitThis で暗黙の any を見つける

this パラメータを「書ける」ことと「書き忘れに気づける」ことは別の話です。書き忘れを検出してくれるのが noImplicitThis というコンパイラオプションで、これは strict: true に含まれています。有効にすると、型を推論できない this がエラーとして報告されます。

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitThis": true
  }
}
implicit-this.ts
function showId() {
  console.log(this.id);
  // エラー: 'this' implicitly has type 'any' because it does not have
  //        a type annotation.
}

// this パラメータを書けば解消する
function showIdSafely(this: HTMLElement): void {
  console.log(this.id);
}

このオプションを切っていると this は暗黙の any になり、どんなプロパティを触ってもエラーになりません。strict を有効にしているプロジェクトなら自動で有効なので、既存コードに strict を導入するとこの種のエラーがまとめて出てくることがあります。1つずつ this パラメータを足していくのが正攻法です。

ThisParameterType と OmitThisParameter

TypeScript には、this パラメータを型として扱うための組み込みユーティリティ型が2つ用意されています。ThisParameterType<T> は関数型 T の this パラメータの型を取り出します(this パラメータがなければ unknown)。OmitThisParameter<T> は逆に this パラメータを取り除いた関数型を作ります。

utility.ts
function toHex(this: number): string {
  return this.toString(16);
}

// this パラメータの型を取り出す → number
type Ctx = ThisParameterType<typeof toHex>;

// this パラメータを取り除いた関数型 → () => string
type Plain = OmitThisParameter<typeof toHex>;

// bind した結果は this パラメータが外れた関数型になる
const fiveToHex: Plain = toHex.bind(5);
console.log(fiveToHex()); // "5"

console.log(toHex.call(255)); // "ff"

OmitThisParameter はジェネリクスが消え、オーバーロードがある場合は最後のシグネチャだけが引き継がれる点に注意してください。日常のコードで直接書く機会は多くありませんが、bind の戻り値の型が「this パラメータの取れた関数」になる仕組みを説明する型なので、覚えておくとエラーメッセージが読みやすくなります。

名前種類説明
this: T構文第1引数の位置に書く仮引数。this の型を宣言する。実引数には含まれずコンパイル後に消える
noImplicitThisコンパイラオプションthis が暗黙の any になる箇所をエラーにする。strict に含まれる
strictBindCallApplyコンパイラオプションbind / call / apply の引数と this を型チェックする。strict に含まれる
ThisParameterType<T>組み込み型関数型から this パラメータの型を取り出す。なければ unknown
OmitThisParameter<T>組み込み型関数型から this パラメータを取り除いた型を作る

アロー関数と call / apply / bind との関係

アロー関数は自分自身の this を持たず、定義された場所の外側の this をそのまま参照します。宣言できる this が存在しないので、this パラメータを書くこと自体が構文エラーになります。

arrow.ts
const bad = (this: HTMLElement) => {
  console.log(this.id);
};
// エラー: An arrow function cannot have a 'this' parameter.

// 逆に言えば、外側の this を使いたいときはアロー関数が正解
class Panel {
  private label = 'panel';

  register(button: HTMLButtonElement): void {
    // アロー関数なので this は Panel のインスタンスのまま
    button.addEventListener('click', () => {
      console.log(this.label);
    });
  }
}

一方、call / apply / bindthis を明示的に指定して呼ぶためのメソッドです。strictBindCallApply(これも strict に含まれます)が有効なら、渡した this が this パラメータの型と合っているかまで検査されます。

bind.ts
function describe(this: { name: string }, suffix: string): string {
  return this.name + suffix;
}

console.log(describe.call({ name: 'yamada' }, 'さん')); // yamadaさん

describe.call({ id: 1 }, 'さん');
// エラー: Argument of type '{ id: number; }' is not assignable to
//        parameter of type '{ name: string; }'.

// bind すると this が固定され、this パラメータのない関数型になる
const describeYamada = describe.bind({ name: 'yamada' });
console.log(describeYamada('さん')); // yamadaさん

this の型が合わずエラーになるとき

「’this’ implicitly has type ‘any’」と表示される

noImplicitThis が有効な状態で、this の型を推論できない関数を書いたときのエラーです。オブジェクトリテラルの中のメソッドやクラスのメソッドなら推論が効きますが、独立した function 宣言では手がかりがないため報告されます。対処は this パラメータを書くことです。どうしても型が決まらない場面(複数の呼ばれ方をする汎用関数など)では this: any と明示すると、意図的であることをコードに残しつつエラーを消せます。

「’this’ コンテキストが割り当てられません」と表示される

this パラメータを宣言した関数を、その this が付いてこない形で呼ぼうとしたときのエラーです。const fn = obj.method のように取り出したり、setTimeout(obj.method, 1000) のようにコールバックとして渡したりすると発生します。型エラーではありますが、実際に実行時のバグが潜んでいるサインでもあります。

直し方は2つです。obj.method.bind(obj) のように this を束縛して渡すか、() => obj.method() とアロー関数で包んで、呼び出しの形を保ったまま渡します。どちらも this が確実に obj になるので、エラーも実行時の不具合も同時に解消します。

「A ‘this’ parameter must be the first parameter」と表示される

this パラメータの位置が2番目以降になっているときのエラーです。function f(value: string, this: Foo) のようには書けません。必ず引数リストの先頭に置いてください。あわせて、名前は必ず this である必要があり、selfctx といった別名では単なる普通の引数として扱われます。

要素の型が一致せず登録できない

this: HTMLButtonElement と書いたハンドラを div に登録しようとすると、リスナーの型が合わずエラーになります。これは this パラメータが正しく働いている証拠なので、登録先の要素を見直すか、両方で使う汎用のハンドラなら this: HTMLElement のように共通の親の型まで広げて宣言します。this パラメータは反変の位置で比較されるため、広い型で宣言しておくほど多くの要素に登録できるようになります。

まとめ

this パラメータは、関数の第1引数の位置に this: 型 と書くだけで、呼び出し方によって変わる this に型を与えられる仕組みです。実際の引数には含まれず、コンパイル後の JavaScript からは完全に消えるため、動作を変えずに安全に追加できます。addEventListener のハンドラを別関数として定義するとき、インターフェースのメソッドで「そのオブジェクトから呼ばれること」を要求したいときが主な出番です。strictnoImplicitThis)を有効にしておけば書き忘れも自動で見つかりますし、strictBindCallApply によって call / apply / bind に渡す this まで検査されます。アロー関数は自身の this を持たないため this パラメータを書けない、という点だけ押さえておけば、this まわりのエラーは落ち着いて読み解けるようになります。

参考ページ