はじめに
JavaScript でおなじみの this ですが、ご存知の通り、様々な落とし穴があります。
(通常のの function と arrow function で挙動が違う、呼び出し元次第で値が変わる、strict モードか否かで挙動が違う、等々)
TypeScript では、this におけるこれらの落とし穴を避けるための 以下の仕組みがあります。
それぞれの仕様をまとめてみました。
この記事では 多様性の this の型(Polymorphic this types)について説明します。
環境
TypeScript: v4.1.3
コード
例 1: Playground Link
例 2: Playground Link
多様性の this の型(Polymorphic this types)
TypeScript では、this を型として扱うことができます。
これはインターフェースやクラスの関数の返り値に使用されます。
返り値に this を指定すると、関数の呼び出し元と同様の型を返すことができます。
例 1 (Set)
JS の組み込みオブジェクトのSetを例に、説明します。
以下は Set を簡略化したものになります。
class Set {
add(value: number): this {}
}
new Set().add(1); // Set
add の返り値を this にしているため、add メソッドの返り値は Set になります。
次に、この Set を拡張したクラスを作成し、delete メソッドを用意します。
class MutableSet extends Set {
delete(value: number) {}
}
new MutableSet().add(1); // MutableSet
// No Error
new MutableSet().add(1).delete(1);
このとき、new MutableSet().add(1)
の返り値の型は Set ではなく、呼び出し元の MutableSet となります。
もし返り値に this ではなく Set を指定した場合、サブクラス(MutableSet)側でスーパークラス(Set)側の呼び出しが困難になります。
class Set {
add(value: number): Set {} // this -> Set
}
new Set().add(1); // Set
class MutableSet extends Set {
delete(value: number) {}
}
new MutableSet().add(1); // Set !
// Property 'delete' does not exist on type 'Set'.ts(2339)
new MutableSet().add(1).delete(1);
class MutableSet extends Set {
delete(value: number) {}
add(value: number): MutableSet {} // 毎回オーバーライドするひつようが出てくる
}
new MutableSet().add(1); // MutableSet
参考: プログラミング TypeScript ――スケールする JavaScript アプリケーション開発
おまけ: 実際の Set の型定義は以下のようになります。
interface Set<T> {
add(value: T): this;
clear(): void;
delete(value: T): boolean;
forEach(
callbackfn: (value: T, value2: T, set: Set<T>) => void,
thisArg?: any
): void;
has(value: T): boolean;
readonly size: number;
}
例 2 (Array)
もうひとつの例として、Array を紹介します。
Array では、every メソッドや sort メソッド の返り値で this を使用しています。
interface Array<T> {
/**
* Determines whether all the members of an array satisfy the specified test.
* @param predicate A function that accepts up to three arguments. The every method calls
* the predicate function for each element in the array until the predicate returns a value
* which is coercible to the Boolean value false, or until the end of the array.
* @param thisArg An object to which the this keyword can refer in the predicate function.
* If thisArg is omitted, undefined is used as the this value.
*/
every<S extends T>(
predicate: (value: T, index: number, array: T[]) => value is S,
thisArg?: any
): this is S[];
/**
* Sorts an array in place.
* This method mutates the array and returns a reference to the same array.
* @param compareFn Function used to determine the order of the elements. It is expected to return
* a negative value if first argument is less than second argument, zero if they're equal and a positive
* value otherwise. If omitted, the elements are sorted in ascending, ASCII character order.
* ```ts
* [11,2,22,1].sort((a, b) => a - b)
* ```
*/
sort(compareFn?: (a: T, b: T) => number): this;
}
特に、every では this をUser-Defined Type Guardsと組み合わせて使用しています。
つまり、every の引数にtype predicate
を返す関数を指定することで、呼び出し元の Array の型を TypeScript に推論させることができます。
const isString = (value: string | number): value is string =>
typeof value === "string";
if (array.every(isString)) {
array; // array: string[]
} else {
array; // array: (string | number)[]
}
こういう使い方もできるんですね。
終わりに
TypeScript ではあまり触れることのない返り値の this ですが、知っておくことで柔軟なクラス定義ができそうです。
参考文献
- プログラミング 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
- User-Defined Type Guards