LoginSignup
0
0

[TypeScript] 変性と Bivariance Hack

Last updated at Posted at 2024-02-25

[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 を使用することもないような気もする…。

参考資料

0
0
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
0
0