最近、これらの知識に触れたばかりで、記事にはいくつかの間違いがあるかもしれません。アドバイスをたくさんいただけると嬉しいです(
TypeScript の学習において、反変、共変、双方向共変、不変について理解することは非常に重要ですが、親子関係の理解さえできれば、これらの概念は簡単に理解できます。したがって、これらの概念を説明する前に、まず親子関係の理解を学ぶ必要があります。
型の親子関係#
まず、TypeScript では、型の構造が同じであれば、親子関係があると言えます。これは Java とは異なります(Java では、extends を使用して継承する必要があります)。
以下の例を見てみましょう:
interface Person {
name: string;
age: number;
}
interface Suemor {
name: string;
age: number;
hobbies: string[]
}
これらの 2 つの型には継承関係があることがわかると思います。この場合、親と子のどちらがどちらか考えてみましょう。
おそらく、Suemor が Person の親タイプであると思うかもしれません(Person には 2 つのプロパティがあり、Suemor には 3 つのプロパティがあり、そのうちの 1 つが Person を含んでいるため)。しかし、これは間違いです。
型システムでは、プロパティがより多い型が子型です。つまり、Suemor は Person の子型です。
これは直感に反するかもしれませんが(私も最初は理解できませんでした)、次のように考えてみてください:A が B を拡張するということは、A は B のプロパティを拡張することができるため、A のプロパティは通常、B よりも多くなります。したがって、A は子型です。または、次の特徴を覚えておくこともできます:子型は親型よりも具体的です。
また、共用型の親子関係を判断するとき、 'a' | 'b' と 'a' | 'b' | 'c' のどちらがより具体的ですか?
'a' | 'b' の方が具体的ですので、'a' | 'b' は 'a' | 'b' | 'c' の子型です。
共変#
オブジェクトでの使用#
共変は理解しやすいですし、日常的によく使用されます。例えば:
interface Person {
name: string;
age: number;
}
interface Suemor {
name: string;
age: number;
hobbies: string[]
}
let person: Person = { // 親型
name: '',
age: 20
};
let suemor: Suemor = { // 子型
name: 'suemor',
age: 20,
hobbies: ['play game', 'codeing']
};
// 正しい
person = suemor;
// エラー、エディターがエラーを表示しない場合は、厳格モードをオンにしてください。なぜなら、後で双方向共変を説明するからです
suemor = person;
これらの 2 つの型は異なりますが、suemor を person に代入できます。つまり、子型は親型に代入できますが、その逆はできません(なぜなら、person を suemor に正しく代入できるとすると、suemor.hobbies
を呼び出すとプログラムが壊れてしまいます)。
したがって、結論は次のとおりです:子型を親型に代入できる場合、それを共変と呼びます。
関数での使用#
関数でも共変を使用することができます。例えば:
interface Person {
name: string;
age: number;
}
function fn(person: Person) {} // 親型
const suemor = { // 子型
name: "suemor",
age: 19,
hobbies: ["play game", "codeing"],
};
fn(suemor);
fn({
name: "suemor",
age: 19,
// エラー
// ここで補足(私が学んだときにミスしたので)、ここでのhobbiesのエラーは、直接代入されているため、型推論が行われていないためです。
hobbies: ["play game", "codeing"]
})
ここでも、hobbies を追加しましたが、共変のため、子型を親型に代入できます。
したがって、私たちが日常的に使用するredux
では、dispatch
の型を宣言する際に次のように書くことができます:
interface Action {
type: string;
}
function dispatch<T extends Action>(action: T) {
}
dispatch({
type: "suemor",
text:'テスト'
});
これにより、渡されるパラメータが必ずAction
の子型であることが制約されます。つまり、type
を持ち、他のプロパティがあっても構いません。
双方向共変#
先ほどの例をもう一度見てみましょう:
interface Person {
name: string;
age: number;
}
interface Suemor {
name: string;
age: number;
hobbies: string[]
}
let person: Person = { // 親型
name: '',
age: 20
};
let suemor: Suemor = { // 子型
name: 'suemor',
age: 20,
hobbies: ['play game', 'codeing']
};
// 正しい
person = suemor;
// エラー -> 双方向共変を設定するとエラーを回避できます
suemor = person;
suemor = person
のエラーは、tsconfig.json
でstrictFunctionTypes:false
を設定するか、厳格モードをオフにすることで回避できます。この場合、親型を子型に代入でき、子型を親型に代入できるようになります。このような場合、これを双方向共変と呼びます。
ただし、これは明らかに問題があり、型の安全性を保証できません。したがって、通常は厳格モードをオンにして、双方向共変を回避します。
不変#
不変は最も単純です。継承関係がない場合(A と B のどちらかが他方のすべてのプロパティを含んでいない場合)、不変です。つまり、非親子関係の型は、型が異なる場合にエラーが発生します。
interface Person {
name: string;
age: number;
}
interface Suemor {
name: string;
sex:boolean
}
let person: Person = {
name: "",
age: 20,
};
let suemor: Suemor = {
name: 'suemor',
sex:true
};
// エラー
person = suemor;
逆変#
逆変は少し理解が難しいかもしれません。以下の例を見てみましょう:
let fn1: (a: string, b: number) => void = (a, b) => {
console.log(a);
};
let fn2: (a: string, b: number, c: boolean) => void = (a, b, c) => {
console.log(c);
};
fn1 = fn2; // エラー
fn2 = fn1; // これは可能
気づくでしょう:fn1 のパラメータは fn2 のパラメータの親型ですが、なぜ子型に代入できるのでしょうか?
これが逆変です。親型は子型に代入できる性質を持ちますが、関数の戻り値は共変です(つまり、子型は親型に代入できます)。
なぜなら、fn1 = fn2
が正しい場合、fn1('suemor',123)
のように呼び出すことしかできず、fn1
の呼び出しでc
を出力することができなくなってしまいます。
したがって、私は逆変が一般的には関数と関数の間で使用されることが多いと感じています(関数の呼び出し時ではなく、関数と関数の間で使用されるという意味ですが、これが私の理解ですが、正しいかどうかはわかりません)。
逆変は、型の演算時によく使用されるため、もう少し難しい例を見てみましょう:
// 戻り値の型を抽出する
type GetReturnType<Func extends Function> = Func extends (
...args: unknown[]
) => infer ReturnType
? ReturnType
: never;
type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;
ここで、GetReturnType
は戻り値の型を抽出するために使用されますが、ReturnTypeResullt
は本来suemor
であるべきですが、上記のコードではnever
となってしまいます。
なぜなら、関数のパラメータは逆変を遵守し、親型しか子型に代入できないためですが、明らかにここではunknown
は{name: string}
の親型であり、逆になってしまいます。したがって、unknown
をstring
の子型に変更する必要があります。つまり、unknown
をany
またはnever
に変更する必要があります。正しい答えは次のとおりです:
type GetReturnType<Func extends Function> = Func extends (
...args: any[]
) => infer ReturnType
? ReturnType
: never;
type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;