はじめに
JavaScript でおなじみの this ですが、ご存知の通り、様々な落とし穴があります。
(通常のの function と arrow function で挙動が違う、呼び出し元次第で値が変わる、strict モードか否かで挙動が違う、等々)
TypeScript では、this におけるこれらの落とし穴を避けるための 以下の仕組みがあります。
それぞれの仕様をまとめてみました。
この記事では、This に関する Utility Type について説明します。
環境
TypeScript: v4.1.3
コード
前提
本記事は以下の前提で書いています。
モード: Strict モード
tsconfig: `noImplicitThis`が有効
This に関する Utility Type
ThisParameterType<Type>
通常、引数の型を取得するParameters
型では、this の型は取得できません。
そこで、ThisParameterType
を使うことで、this パラメーターの型を取得することができます。
function fn(this: { name: string }, age: number): string {
return this.name;
}
// type Fn = (this: {
// name: string;
// }, age: number) => string
type Fn = typeof fn;
// type Param = [age: number]
type Param = Parameters<Fn>;
// type FnThisParameterType = {
// name: string;
// }
type FnThisParameterType = ThisParameterType<Fn>;
OmitThisParameter<Type>
関数の型から、this パラメーターの型を除いたものを取得します。
function fn(this: { name: string }, age: number): string {
return this.name;
}
type Fn = typeof fn;
// type FnOmitThisParameter = (age: number) => string
type FnOmitThisParameter = OmitThisParameter<Fn>;
これは、ThisParameterType
と合わせて関数のbind メソッドの型で使われています。
bind メソッドの型は以下のように定義されます。
interface CallableFunction extends Function {
// ThisParameterTypeを使用しているオーバーロードのみ表示
bind<T>(this: T, thisArg: ThisParameterType<T>): OmitThisParameter<T>;
}
これは、
- 引数
thisArg
に、レシーバの関数T
の this パラメーターを要求 - 返り値は、レシーバの関数
T
から this パラメーターを取り除いたものを返却
という意味合いになります。
実際に上で宣言した fn 関数の bind を呼び出すと以下のようになります。
function fn(this: { name: string }, age: number): string {
return this.name;
}
// const bound: (age: number) => string
const bound = fn.bind({ name: "foo" });
bound(2); //コンテキストは this:void だが、エラーにならない
// Argument of type '{ age: number; }' is not assignable to parameter of type '{ name: string; }'.
fn.bind({ age: 1 });
ThisParameterType<T>
は{name: string}
となるため、bind メソッドに{age: 1}
を渡した場合、型エラーとなります。
また、OmitThisParameter<T>
は(age: number) => string
となります。
これにより、bind メソッドの返り値の関数に対して、fn
で定義した this とコンテキストの異なる状態で実行してもエラーになりません。
ThisType<Type>
<Type>に与えられたオブジェクトのコンテキストにおける this の型を定義します。
以下の例では、obj の型にThisType<FnThisParameterType>
を指定しています。
つまり、この obj 内で使用できる this の型がFnThisParameterType
となります。
FnThisParameterType
は name property だけを持つ Interface です。
そのため、obj.fn
ではthis.name
は参照できますが、this.age
は参照できません。
function fn(this: { name: string }, age: number): string {
return this.name;
}
type Fn = typeof fn;
type FnThisParameterType = ThisParameterType<Fn>;
const obj: ThisType<FnThisParameterType> = {
fn() {
this.name; // OK
this.age; // Property 'age' does not exist on type '{ name: string; }'.ts(2339)
},
};
使い所が無さそうに見えるのですが、私の身近なところではVue.extends
に使われていました。
Vue では、props や data で宣言した値を、computed や methods 内の this から参照することができます。
さらに、Vue.extends
を使うと this について型推論することもできます。
var Profile = Vue.extend({
template: "<p>{{firstName}} {{lastName}} aka {{alias}}</p>",
data: function () {
return {
firstName: "Walter",
lastName: "White",
alias: "Heisenberg",
};
},
methods: {
getName() {
// firstName, lastName, aliasが型推論される
return this.firstName + this.lastName;
},
},
});
この実装に、ThisType が使われていました。
どうやら、data や prop, computed 等々をまとめたものを ThisType に渡し、その結果を Vue.extends の引数の options に指定しているようです。
export type ThisTypedComponentOptionsWithArrayProps<
V extends Vue,
Data,
Methods,
Computed,
PropNames extends string
> = object &
ComponentOptions<
V,
DataDef<Data, Record<PropNames, any>, V>,
Methods,
Computed,
PropNames[],
Record<PropNames, any>
> &
ThisType<
// here
CombinedVueInstance<
V,
Data,
Methods,
Computed,
Readonly<Record<PropNames, any>>
>
>;
export interface VueConstructor<V extends Vue = Vue> {
new <
Data = object,
Methods = object,
Computed = object,
PropNames extends string = never
>(
options?: ThisTypedComponentOptionsWithArrayProps<
V,
Data,
Methods,
Computed,
PropNames
>
): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>;
// ...
}
引用:
最後に
個人的には一番最後のThisType
に感動しました。
Vue options api のあの不思議な型推論は、これを使って実装されていたんですね……。