2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【TypeScript】 this のトリセツ(2. This に関する Utility Type)

Last updated at Posted at 2021-01-08

はじめに

JavaScript でおなじみの this ですが、ご存知の通り、様々な落とし穴があります。
(通常のの function と arrow function で挙動が違う、呼び出し元次第で値が変わる、strict モードか否かで挙動が違う、等々)

TypeScript では、this におけるこれらの落とし穴を避けるための 以下の仕組みがあります。

  1. this パラメーター
  2. This に関する Utility Type
  3. 多様性の this の型(Polymorphic this types)

それぞれの仕様をまとめてみました。

この記事では、This に関する Utility Type について説明します。

環境

TypeScript: v4.1.3

コード

Playground Link

前提

本記事は以下の前提で書いています。

モード: 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 のあの不思議な型推論は、これを使って実装されていたんですね……。

参考文献

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?