はじめに
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 のオプション strictFunctionTypes
が true
の場合に限ります。
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 | null
を number
に代入できないからです。」という内容になります。
このような違いは 変性(variance) の違いによるものです。
関数の引数は 反変 (contravariant) であるのに対し、メソッドの引数は 双変 (bivariant) です。
雑に説明すると、メソッドの型よりも関数の型の方が安全ということです。
変性 について詳しく知りたい方は以前書いた以下の記事を参考にしてみてください。
なぜこのような違いがあるかというと、関数の型も以前はメソッドと同様に双変でした。ですが、 strictFunctionTypes
オプションによって関数の引数の変性を双変から反変になった際に、メソッドは双変のままだったからです。
メソッドは双変性を活用した bivarianceHack について
strictFunctionTypes
が true
となっている環境で、あえて引数が双変の関数を作るテクニックとして bivarianceHack というものがあります。やり方は簡単で、以下のようにメソッドの記法でプロパティを持つオブジェクトの型を用意し、そこからそのプロパティの型を取り出すだけです。
type bivariantFunc = { bivarianceHack(arg: A): B }['bivarianceHack']
このテクニックが役に立つ例として、strictFunctionTypes
を false
から true
に変更する際に、変性が変わることによりエラーになってしまう場合でも bivarianceHack で一時的にエラーを回避することができます。
他にも、React の型定義にもこのテクニックが使用されていたりします。
// Bivariance hack for consistent unsoundness with RefObject
type RefCallback<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"];
おわりに
この記事を書いたきっかけは、strictFunctionTypes
が true
の環境で React を書いていた際に、「この書き方だとエラーになるはずだけど、なぜかエラーにならない!」という場面があり、調べてみたところこの違いを知りました。
TypeScript の型は奥深く、書いていると新しい発見がありとても楽しい言語です!今後もまた何か発見したら記事にしていこうと思います!