8
3

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 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

【TypeScript】関数の型とメソッドの型の違いと bivarianceHack について

Posted at

はじめに

TypeScript で、関数をプロパティに持つオブジェクトの型を定義する方法として、以下の func のように関数の型を指定する方法と、method のようにメソッドとして型を指定する方法があります。

type Obj = {
  func: (arg: number | null) => void
  method(arg: number | null): void
}

この2つの関数の型には違いがあります。この記事ではその違いについて説明し、さらにそれが活用されている bivarianceHack について紹介します。

上記で使用しているアロー関数のように型を書く方法 (func1) とは別に、オーバーロードなどで利用する関数の型の記法 (func2) がありますが、型としては同じであるため本記事では省略します。

type Obj = {
  func1: (arg: number | null) => void
  func2: { (arg: number | null): void }
}

関数の型とメソッドの型の違い

関数とメソッドをプロパティに持つオブジェクトを実際に作って型を確認してみます。

type Obj = {
  func: (arg: number | null) => void
  method(arg: number | null): void
}

const obj: Obj = {
  func: (arg: number | null) => {},
  method(arg: number | null) {},
}

// それぞれのプロパティを変数に代入して、型を確認してみる
const objFunc   = obj.func
const objMethod = obj.method

TypeScript Playground で上記を入力してみます。

すると、以下のように、どちらも (arg: number | null) => void になっています。

declare const objFunc: (arg: number | null) => void;
declare const objMethod: (arg: number | null) => void;

ですが、実は以下のようにそれぞれの型を指定した変数に (arg: number) => {} を代入しようとすると、func はエラーになりますが、 method はエラーになりません。

ただし、TypeScript のオプション strictFunctionTypestrue の場合に限ります。

const f: Obj['func']   = (arg: number) => {} // error
const m: Obj['method'] = (arg: number) => {} // ok

エラーは以下のようになっています。

Type '(arg: number) => void' is not assignable to type '(arg: number | null) => void'.
  Types of parameters 'arg' and 'arg' are incompatible.
    Type 'number | null' is not assignable to type 'number'.
      Type 'null' is not assignable to type 'number'.

要約すると、「(arg: number) => void(arg: number | null) => void に代入できません。なぜなら number | nullnumber に代入できないからです。」という内容になります。

このような違いは 変性(variance) の違いによるものです。
関数の引数は 反変 (contravariant) であるのに対し、メソッドの引数は 双変 (bivariant) です。
雑に説明すると、メソッドの型よりも関数の型の方が安全ということです。

変性 について詳しく知りたい方は以前書いた以下の記事を参考にしてみてください。

なぜこのような違いがあるかというと、関数の型も以前はメソッドと同様に双変でした。ですが、 strictFunctionTypes オプションによって関数の引数の変性を双変から反変になった際に、メソッドは双変のままだったからです。

メソッドは双変性を活用した bivarianceHack について

strictFunctionTypestrue となっている環境で、あえて引数が双変の関数を作るテクニックとして bivarianceHack というものがあります。やり方は簡単で、以下のようにメソッドの記法でプロパティを持つオブジェクトの型を用意し、そこからそのプロパティの型を取り出すだけです。

type bivariantFunc = { bivarianceHack(arg: A): B }['bivarianceHack']

このテクニックが役に立つ例として、strictFunctionTypesfalse から true に変更する際に、変性が変わることによりエラーになってしまう場合でも bivarianceHack で一時的にエラーを回避することができます。

他にも、React の型定義にもこのテクニックが使用されていたりします。

    // Bivariance hack for consistent unsoundness with RefObject
    type RefCallback<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"];

おわりに

この記事を書いたきっかけは、strictFunctionTypestrue の環境で React を書いていた際に、「この書き方だとエラーになるはずだけど、なぜかエラーにならない!」という場面があり、調べてみたところこの違いを知りました。

TypeScript の型は奥深く、書いていると新しい発見がありとても楽しい言語です!今後もまた何か発見したら記事にしていこうと思います!

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?