TypeScriptにおける変性、変性アノテーションてなんすか?
最近OSSのコードを見ていた時に、
👀 「これなんすか?」
↓
class A<in out T>{...}
となったので、これ について調べてみました。
※ これ は今回の記事のテーマでもある、変性アノテーション です。
ドキュメントとリリースノートをちゃんと読んでいる人からしたら何でもない内容かもしれないですが、せっかく調べたので記事にしました。
TypeScriptにおける変性、変性アノテーションについて学びたい人の役に立てば嬉しいです(?)
想定読者
- 変性ってなんすか?てなった人
- TypeScriptの
in
(in
演算子ではない),out
ってなんすか?てなった人
要約
- 変性とは
任意の型に対して、対象となるもう一方の型が持つべき性質のこと - 変性アノテーション
in
:型パラメータが反変(contravariant)であることを示す
out
:型パラメータが共変(covariant)であることを示す
in out
:型パラメータが不変(invariant)であることを示す
変性(variance)とは?
変性についての理解する前に、サブタイプ(派生型)・スーパタイプ(上位型)について理解しておく必要があります。
サブタイプ・スーパタイプ?となった方はこちら
サブタイプ・スーパタイプとは 型の関係性 を表現する概念です。
▼ サブタイプ
type T
と type U
という2つの型があり、
type U
がtype T
のサブタイプ(派生型)である場合、
type T
が要求されるところでは 型安全に type U
を使うことができる。
(type T
のサブタイプがtype U
である場合、type T
の部分集合となるものがtype U
)
▼ スーパータイプ
type T
と type U
という2つの型があり、
type U
がtype 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では オブジェクト、クラス、配列、および関数の戻り値の型 は、そのメンバーに対して共変です。
👀 配列が共変になっている理由について気になったかたは、こちらの記事をご覧ください
関数の変性
関数では 戻り値 と 引数 で変性が異なります。
具体的には、戻り値の場合は共変、引数の場合は反変の性質を持ちます。
▼ 戻り値の場合の例(共変性)
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 Server の class ApolloServer<...>({...}){...}
で使われています。
使用している理由はコメントで書いてくれている通りですが、型引数が不変であることを明示的にするために 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でも...
終わりに
この記事では、変性と変性アノテーションについてまとめました。
変性・変性アノテーションについて知りたい人の役に立っていれば嬉しいです。
最後まで読んでいただきありがとうございました!
記事についての疑問やご指摘があれば是非、コメントやTwitter(X)からお願いします。
Discussion