method_missing(*)

TypeScriptにおける変性

2022-03-17 (Thu)

概要

型について理解を深める上で必要不可欠となる変性(Variance)をTypescriptを通して学んだので、まとめておく。


サブタイプとスーパータイプ

まずは変性について学ぶ上で、知っておくべき基本的な用語から触れていく。

サブタイプ

A, Bという2つの型があり、BがAのサブタイプである場合、Aが要求されているところはどこでも、Bを安全に使うことができる

TypeScriptによる例を以下に示す。

  • 配列はオブジェクトのサブタイプ
  • タプルは配列のサブタイプ
  • 全ての値はanyのサブタイプ
  • neverは全ての値のサブタイプ
  • Animalを拡張するBirdというクラスがある場合、BirdAnimalのサブタイプ

スーパータイプ

A, Bという2つの型があり、BがAのスーパータイプである場合、Bが要求されているところ はどこでも、Aを安全に使うことができる。

TypeScriptによる例を以下に示す。

  • オブジェクトは配列のスーパータイプ
  • 配列はタプルのスーパータイプ
  • anyは全ての値のスーパータイプ
  • すべての値はneverのスーパータイプ
  • AnimalBirdのスーパータイプ

変性とは

型同士の関係性(型Aを指定したときに、そのサブタイプである型Bを当てはめられるかどうか)を判別するルールは、プログラム言語間で大きな相違点となっている

このようなルールを一般的に指す言葉が変性であり、以下の4種類に分けられる。

不変性

Tそのものを必要とする。

共変性

<:Tであるものを必要とする、つまりTそのものあるいはTのサブタイプを必要とする。

反変性

>:Tであるものを必要とする、つまりTそのものあるいはTのスーパータイプを必要とする。

双変性

<:T または >:Tのどちらかを必要とする、つまりTそのものあるいはTのサブタイプ、もしくはスーパータイプを必要とする。


TypeScriptにおける変性

TypeScriptでは、オブジェクト、クラス、配列、関数の戻り値の型のような複雑な型はすべて、そのメンバーに対して共変である。

以下に共変であることの例を示す。

class Animal {}

// Animalのサブクラス
class Bird extends Animal {
  chirp() {}
}

// Birdのサブクラス
class Crow extends Bird {
  caw() {}
}

const generate = (f: () => Bird) => {
  // ...
}

// Animalはgenerateで受け取る関数の戻り値の型のスーパータイプなのでNG
// Error: Argument of type '() => Animal' is not assignable to parameter of type '() => Bird'.
// Property 'chirp' is missing in type 'Animal' but required in type 'Bird'.
const toAnimal = (): Animal => new Animal
generate(toAnimal)

// Birdはgenerateで受け取る関数の戻り値の型と同一なのでOK
const toBird = (): Bird => new Bird
generate(toBird)

// Crowはgenerateで受け取る関数の戻り値の型のサブタイプなのでOK
const toCrow = (): Crow => new Crow
generate(toCrow)

ただし一つだけ例外が存在し、それは関数のパラメータの型

strictFunctionTypesがオンの場合、関数のパラメータの型は共変ではなく反変、つまりサブタイプの場合はコンパイルエラーが起きる。

以下に例を示す。

const clone = (f: (b: Bird) => Bird) => {
  // ...
}

// a: Animal はcloneの引数の関数パラメータ型 b: Bird のスーパータイプなのでOK
const animalToBird = (a: Animal): Bird => new Bird
clone(animalToBird)

// b: Bird はcloneの引数の関数パラメータ型 b: Bird と同一なのでOK
const birdToBird = (b: Bird): Bird => b
clone(birdToBird)

// c: Crow はcloneの引数の関数パラメータ型 b: Bird のサブタイプなのでNG
const crowToBird = (c: Crow): Bird => new Crow
// Error: Argument of type '(c: Crow) => Bird' is not assignable to parameter of type '(b: Bird) => Bird'.
// Types of parameters 'c' and 'b' are incompatible.
// Property 'caw' is missing in type 'Bird' but required in type 'Crow'.
clone(crowToBird)

strictFunctionTypesがオフの場合は双変、つまり上記のコードは全て通るようになる。


まとめ

上記のことは覚えておいてもよいが、なかなか難しいと思われる(少なくとも私は無理)。

なのでワードだけ頭の片隅に入れておき、誤った型付けをしてIDEに怒られた際に「こんなのあったな〜」と思い出して調べることで原因特定の材料とすれば良い。


参考文献