[TypeScript] 変性と Bivariance Hack

# [TypeScript] 変性と Bivariance Hack

React の型定義を覗いていると `bivarianceHack` なるものがあったため、TypeScript における変性と併せて調べてみる。

## 変性について

変性とは簡単に言うと、値が表現できる範囲を表す型システム上の性質である。
変性にはその範囲に応じて「共変性」「反変性」「双変性」「非変性」がある。

例えば、以下の様な型と値があるとする。
この型 `A` は型 `B` より狭く、型 `C` は型 `B` より広い型である。
これらに対して、それぞれの変性を見てみる。

```ts
type A = number;
type B = number | null;
type C = number | null | undefined;

const arrayA: Array<A> = [];
const arrayB: Array<B> = [];
const arrayC: Array<C> = [];
```

### 共変性 (co-variance)

```ts
// 以下が成り立つ時、値は共変性である。
let value: Array<B>;
value = arrayA;  // OK
value = arrayB;  // OK
value = arrayC;  // Error
```

* より狭い型を受け付ける性質。
* 通常の値や関数の返り値などがこれにあたる。

### 反変性 (contra-variance)

```ts
// 以下が成り立つ時、値は反変性である。
let value: Array<B>;
value = arrayA;  // Error
value = arrayB;  // OK
value = arrayC;  // OK
```

* より広い型を受け付ける性質。
* TypeScript においては、関数のパラメータ（引数）がこれにあたる（詳細は後述）。

### 双変性 (bi-variance)

```ts
// 以下が成り立つ時、値は双変性である。
let value: Array<B>;
value = arrayA;  // OK
value = arrayB;  // OK
value = arrayC;  // OK
```

* 共変性かつ反変性である（より狭いまたは広い型を受け付ける）性質。
* 一部でも重複していればよい、かなり曖昧な範囲。

### 非変性、不変性 (in-variance)

```ts
// 以下が成り立つ時、値は非変性である。
let value: Array<B>;
value = arrayA;  // Error
value = arrayB;  // OK
value = arrayC;  // Error
```

* 共変性でも反変性でもない（自身のみを受け付ける）性質。
* TypeScript ではあまり見ない？

## 関数のパラメータの変性

```ts
// 型 A, B, C は前の例と同様。
type Method = (value: B) => void;
const methodA: Method = (value: A) => { /* */ };  // Error
const methodB: Method = (value: B) => { /* */ };  // OK
const methodC: Method = (value: C) => { /* */ };  // OK
```

前述した様に TypeScript における関数のパラメータは反変性である。
上記の例でみると、`Method` 型のパラメータには `number` 又は `null` が指定される可能性があるため、`number` 型しか考慮していない型 `A` の関数はエラーが発生する。

ただし以上は、TypeScript 設定の `strictFunctionTypes` が有効になっている時の話である。
この設定が無効になっている時は、関数のパラメータは双変性になり、上記例のコードもエラーが発生しなくなる。
これは型システム上安全ではないため、`strictFunctionTypes`（または `strict`）は有効にすることが推奨されている。

## メソッド型と関数型

関数の型を定義する時、以下の様に関数式の様に定義する「メソッド型」とアロー関数式の様に定義する「関数型」の 2 種類の書き方がある。

```ts
type Foo = { method(value: B): void };      // メソッド型
type Bar = { method: (value: B) => void };  // 関数型
```

一見これらの型に違いはないように見えるが、実際は関数のパラメータの変性が異なっている。
アロー関数式の様に定義する「関数型」は `strictFunctionTypes` を影響を受ける（反変性である）が、関数式の様に定義する「メソッド型」は `strictFunctionTypes` の影響を受けない（常に双変性である）。

```ts
// strictFunctionTypes が有効な時：
const foo: Foo = { method(value: A) { /* */ } };  // OK（双変性）
const bar: Bar = { method(value: A) { /* */ } };  // Error（反変性）
```

前に述べた通り、関数のパラメータは反変性であることが望ましいため、アロー関数式の様に定義する「関数型」の方が型安全である。

が、実際の開発でそこまで気にすることもあまりない…。

## `bivarianceHack` とは

冒頭の話題に戻り、React の型定義にも含まれている Bivariance Hack とは、前述のメソッド型を利用して（設定に依らず）関数のパラメータを双変性にすることである。
例えば以下の様に関数と他の型の共用型を定義する時、そのまま記述すると関数型になりパラメータは反変性になる。

```ts
// 関数のパラメータは反変性
type Foo = { 
  method: ((value: B) => void) | undefined;
};
```

通常は安全のためこれでいいのだが、Bivariance Hack ではこれを以下の様に書き換えることで、無理やりメソッド型を使用して関数のパラメータを双変性にしている。

```ts
// 関数のパラメータは双変性
type Foo = {
  method: ({
    bivarianceHack(method: A): void;
  }["bivarianceHack"]) | undefined
};
```

これを利用することで、React 等の一部のライブラリでは型システム上の汎用性を上げるている。
ただしパラメータを双変性にすることは型安全性を犠牲にすることでもあるため、無暗に使用できるものではない。

## おわりに

* 値や関数の返り値などは共変性
* 関数のパラメータは反変性（または双変性）
* 関数型とメソッド型で結果が異なる

ただし、通常の開発でこれらを意識する必要も、Bivariance Hack を使用することもないような気もする…。

## 参考資料

* [strictFunctionTypes | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/tsconfig/strictfunctiontypes)