[TypeScript] 変性と Bivariance Hack
React の型定義を覗いていると bivarianceHack
なるものがあったため、TypeScript における変性と併せて調べてみる。
変性について
変性とは簡単に言うと、値が表現できる範囲を表す型システム上の性質である。
変性にはその範囲に応じて「共変性」「反変性」「双変性」「非変性」がある。
例えば、以下の様な型と値があるとする。
この型 A
は型 B
より狭く、型 C
は型 B
より広い型である。
これらに対して、それぞれの変性を見てみる。
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)
// 以下が成り立つ時、値は共変性である。
let value: Array<B>;
value = arrayA; // OK
value = arrayB; // OK
value = arrayC; // Error
- より狭い型を受け付ける性質。
- 通常の値や関数の返り値などがこれにあたる。
反変性 (contra-variance)
// 以下が成り立つ時、値は反変性である。
let value: Array<B>;
value = arrayA; // Error
value = arrayB; // OK
value = arrayC; // OK
- より広い型を受け付ける性質。
- TypeScript においては、関数のパラメータ(引数)がこれにあたる(詳細は後述)。
双変性 (bi-variance)
// 以下が成り立つ時、値は双変性である。
let value: Array<B>;
value = arrayA; // OK
value = arrayB; // OK
value = arrayC; // OK
- 共変性かつ反変性である(より狭いまたは広い型を受け付ける)性質。
- 一部でも重複していればよい、かなり曖昧な範囲。
非変性、不変性 (in-variance)
// 以下が成り立つ時、値は非変性である。
let value: Array<B>;
value = arrayA; // Error
value = arrayB; // OK
value = arrayC; // Error
- 共変性でも反変性でもない(自身のみを受け付ける)性質。
- TypeScript ではあまり見ない?
関数のパラメータの変性
// 型 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 種類の書き方がある。
type Foo = { method(value: B): void }; // メソッド型
type Bar = { method: (value: B) => void }; // 関数型
一見これらの型に違いはないように見えるが、実際は関数のパラメータの変性が異なっている。
アロー関数式の様に定義する「関数型」は strictFunctionTypes
を影響を受ける(反変性である)が、関数式の様に定義する「メソッド型」は strictFunctionTypes
の影響を受けない(常に双変性である)。
// strictFunctionTypes が有効な時:
const foo: Foo = { method(value: A) { /* */ } }; // OK(双変性)
const bar: Bar = { method(value: A) { /* */ } }; // Error(反変性)
前に述べた通り、関数のパラメータは反変性であることが望ましいため、アロー関数式の様に定義する「関数型」の方が型安全である。
が、実際の開発でそこまで気にすることもあまりない…。
bivarianceHack
とは
冒頭の話題に戻り、React の型定義にも含まれている Bivariance Hack とは、前述のメソッド型を利用して(設定に依らず)関数のパラメータを双変性にすることである。
例えば以下の様に関数と他の型の共用型を定義する時、そのまま記述すると関数型になりパラメータは反変性になる。
// 関数のパラメータは反変性
type Foo = {
method: ((value: B) => void) | undefined;
};
通常は安全のためこれでいいのだが、Bivariance Hack ではこれを以下の様に書き換えることで、無理やりメソッド型を使用して関数のパラメータを双変性にしている。
// 関数のパラメータは双変性
type Foo = {
method: ({
bivarianceHack(method: A): void;
}["bivarianceHack"]) | undefined
};
これを利用することで、React 等の一部のライブラリでは型システム上の汎用性を上げるている。
ただしパラメータを双変性にすることは型安全性を犠牲にすることでもあるため、無暗に使用できるものではない。
おわりに
- 値や関数の返り値などは共変性
- 関数のパラメータは反変性(または双変性)
- 関数型とメソッド型で結果が異なる
ただし、通常の開発でこれらを意識する必要も、Bivariance Hack を使用することもないような気もする…。