🔍

TypeScriptにおける変性、変性アノテーションてなんすか?

2024/06/24に公開

最近OSSのコードを見ていた時に、

👀 「これなんすか?」
↓
class A<in out T>{...} 

となったので、これ について調べてみました。
これ は今回の記事のテーマでもある、変性アノテーション です。

ドキュメントとリリースノートをちゃんと読んでいる人からしたら何でもない内容かもしれないですが、せっかく調べたので記事にしました。

TypeScriptにおける変性、変性アノテーションについて学びたい人の役に立てば嬉しいです(?)

想定読者

  • 変性ってなんすか?てなった人
  • TypeScriptの inin演算子ではない), outってなんすか?てなった人

要約

  • 変性とは
    任意の型に対して、対象となるもう一方の型が持つべき性質のこと
  • 変性アノテーション
    in:型パラメータが反変(contravariant)であることを示す
    out:型パラメータが共変(covariant)であることを示す
    in out:型パラメータが不変(invariant)であることを示す

変性(variance)とは?

変性についての理解する前に、サブタイプ(派生型)・スーパタイプ(上位型)について理解しておく必要があります。

サブタイプ・スーパタイプ?となった方はこちら

サブタイプ・スーパタイプとは 型の関係性 を表現する概念です。

▼ サブタイプ

type Ttype U という2つの型があり、
type Utype T のサブタイプ(派生型)である場合、
type Tが要求されるところでは 型安全に type U を使うことができる。
type Tのサブタイプがtype Uである場合、type Tの部分集合となるものがtype U

▼ スーパータイプ

type Ttype U という2つの型があり、
type Utype T のスーパータイプ(上位型)である場合、
type Uが要求されるところでは 型安全に type T を使うことができる。
type Tのスーパータイプがtype Uである場合、type Tの和集合となるものがtype U

// type A は type B のスーパータイプであり、type B は type A のサブタイプ
type A = number;
type B = 1;

// type C は type D のスーパータイプであり、type D は type C のサブタイプ
type C = number | string;
type D = "hoge";

// type E は type F のスーパータイプであり、type F は type E のサブタイプ
type E = any;
type F = 1;

// type G は type H のスーパータイプであり、type H は type G のサブタイプ
type G = {
    a: number
};
type H = {
    a: number
    b: string
}


変性とは

TypeScriptにおける変性とは、任意の型に対して、対象となるもう一方の型が持つべき性質 を意味します。
変性には4つの種類があります。(例:型 Tが ~性 である場合に、対象となるもう一方の型がどのような性質を持つべきか)

  • 不変性(invariance)
    T そのもの
  • 共変性(covariance)
    T もしくは T のサブタイプ
  • 反変性(contravariance)
    T もしくは T のスーパータイプ
  • 双変性(bivariance)
    T もしくは T のサブタイプ もしくは T のスーパータイプ

これ見るだけでは、イメージが湧かないと思うので例を挙げながら説明していきます。

共変性の例(オブジェクト)

共変性について、オブジェクトを例に説明します。

A と 型 Bが定義されています。
A は型 B にとってのスーパータイプであり、型 B は 型 A にとってのサブタイプになっています。

type A = {
    hoge: string;
}

type B = {
    hoge: string;
    huga: "b"
}
let a: A = { hoge: "..." };
let b: B = { hoge: "...", huga: "b" }

a = a; // ok
a = b; // ok
b = a; // error

a に対して、a, bは代入できますが、bに対してaは代入できません。

  • a に対して、aは代入できる => aに対してaは そのもの
  • a に対して、bは代入できる => aにとってbはサブタイプ
  • b に対して、aは代入できない => bにとってaはスーパータイプ

共変性(covariance)の ある型が存在する場合、もう一方は そのもの もしくは そのもののサブタイプ という性質に 振る舞い が一致しており、オブジェクトは共変であることがわかります。

TypeScriptでは オブジェクト、クラス、配列、および関数の戻り値の型 は、そのメンバーに対して共変です。

👀 配列が共変になっている理由について気になったかたは、こちらの記事をご覧ください
https://typescriptbook.jp/reference/values-types-variables/array/array-type-is-covariant#typescriptで配列が共変になっている理由

関数の変性

関数では 戻り値引数 で変性が異なります。
具体的には、戻り値の場合は共変、引数の場合は反変の性質を持ちます。

▼ 戻り値の場合の例(共変性)

type A = {
    hoge: string
};
type B = {
    hoge: string
    huga: 1
}

const a = { hoge: "..." };
const b = { hoge: "...", huga: 1 };

// ok `type A`は`type A`を満たしているのでエラーが出ない( A は A そのもの )
const fn1: () => A = () => a 

// ok `type B`は`type A`を満たしているのでエラーが出ない( B は A のサブタイプ )
const fn2: () => A = () => b 

// error `type A`は`type B`を満たしていないのでエラーが出る( A は B のスーパータイプ )
const fn3: () => B = () => a 

▼ 引数の場合の例(反変性)

type A = {
    hoge: string
};
type B = {
    hoge: string
    huga: 1
}

const a = { hoge: "..." };
const b = { hoge: "...", huga: 1 };

// ok 型引数 A は A を満たしているので型エラーが出ない( A は A そのもの )
const fn1: (arg: A) => void = (arg: A) => {
    console.log(a.hoge)
}  

// error 型引数 A は B を満たしていないので型エラーが出る( A は B のスーパータイプ )
const fn2: (arg: A) => void = (arg: B) => {
    console.log(a.hoge)
    console.log(a.huga)
}

// ok 型引数 B は A を満たしていないので型エラーが出ない( B は A のサブタイプ )
const fn3: (arg: B) => void = (arg: A) => {
    console.log(a.hoge)
}

変性アノテーション in out について

TypeScriptで定義できる変性アノテーションは3つです。

  • in
    型パラメータが反変(contravariant)であることを示す
  • out
    型パラメータが共変(covariant)であることを示す
  • in out
    型パラメータが不変(invariant)であることを示す

TypeScriptで変性アノテーションを使うメリットは2つあります。

  • 型パラメータがどのように使用されているのかを明示できる

  • 型チェックにおける精度と速度を高められる
    TypeScriptでは型パラメータの変性を推測することで、大きな構造型の型チェックの時間を短縮しています。しかしこの変性の推測(計算)にはかなりのコストがかかります。そこで明示的なアノテーションを与えることで、型チェックの時間を短縮(高速化)と精度向上が実現できます。

正しい使い方と誤った使い方の例です。

`in`の例
// ok
type Getter<out T> = () => T; // 関数の戻り値の型は 型パラメータ T に対して、反変の性質を持つべき ということを `out` で明示している
// error
type Getter<in T> = () => T;`out`の例
// ok
type Setter<in T> = (value: T) => void; // 関数の引数の型は 型パラメータ T に対して、共変の性質を持つべき ということを `in` で明示している
// error
type Setter<out T> = (value: T) => void;`in out`の例
// 型パラメータ が 引数と戻り値の両方で使用される場合、その型パラメータは不変になる
// ok 
interface State<in out T> { // 型パラメータ T は 不変の性質を持つべき ということを `in out` で明示している
  get: () => T;
  set: (value: T) => void;
}
// error
interface State<in T> {...}
// error
interface State<in T> {...}
// error 'in'
interface State<out in T> {...}

[おまけ] 変性アノテーションを使っているOSS

変性アノテーションは、有名なOSS(?)だと Apollo Serverclass ApolloServer<...>({...}){...}で使われています。
https://github.com/apollographql/apollo-server/blob/main/packages/server/src/ApolloServer.ts#L215

使用している理由はコメントで書いてくれている通りですが、型引数が不変であることを明示的にするために in outを使用しています。

意訳
ApolloServerでは 型引数である TContext が不変(invariant)であることを宣言したいので、in outを使用している

例1:特定の型(ApolloServer<{importantContextField: boolean}>)でなくてはならないのに、制約が少ない変数(型ApolloServer<{}>)に代入して、重要なフィールドが欠けている値を渡す

const s: ApolloServer<{}> =
new ApolloServer<{importantContextField: boolean}>({ ... });
s.executeOperation({query}, {contextValue: {}})

例2:最初に宣言した型(ApolloServer<{}>)より制約のある変数(型ApolloServer<{importantContextField: boolean}>)に代入する

const sBase = new ApolloServer<{}>({ ... })const s: ApolloServer<{importantContextField: boolean}> = sBase;
s.addPlugin({async requestDidStart({contextValue: {importantContextField}}) { ... }})
sBase.executeOperation({query}, {contextValue: {}})

あとSolid.jsでも...
https://github.com/solidjs/solid/blob/main/packages/solid/src/reactive/signal.ts#L180-L187

終わりに

この記事では、変性と変性アノテーションについてまとめました。
変性・変性アノテーションについて知りたい人の役に立っていれば嬉しいです。

最後まで読んでいただきありがとうございました!
記事についての疑問やご指摘があれば是非、コメントやTwitter(X)からお願いします。

参考文献

CHILLNN Tech Blog

Discussion