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

# はじめに

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

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

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

:::note

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

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

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

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

```ts
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 で上記を入力してみます。

https://www.typescriptlang.org/play?#code/C4TwDgpgBA8gRgKygXigbwFBSgMwK4B2AxgFxQAUAhgE4DmZBeAtnBNVAD5SMA2PAlCgB8UAG4B7AJYATLFCYRgAC3HSqdBs1bsuvAWQkyMAXwwYi4ggGdgUcYjLwkqTNnzEy6+ty1tOPvkFkETRjABo5BWVVTxpvRhY-XTxA4XRwkzMLa1t7BAAxQiJsFDtEADp3InNLGzKEAFlFFWlSvPKolqza2xxHRABtAHIqoYBdEtQvTUTqIJDTbLqmfoRhztVx0umfWfn0jCA

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

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

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

:::note warn
ただし、TypeScript のオプション `strictFunctionTypes` が `true` の場合に限ります。
:::

```typescript
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) です。
雑に説明すると、メソッドの型よりも関数の型の方が安全ということです。

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

https://qiita.com/wataru86/items/9956feb5b0f42fd49dc9

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

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

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

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

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

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

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

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/3d5f320ea82cfb2077687560927df712c0da9f9d/types/react/index.d.ts#L91-L92

# おわりに

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

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