最近、これらの知識に触れたばかりで、記事にはいくつかの間違いがあるかもしれませんが、多くのアドバイスをいただければ幸いです(
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 つのプロパティがあり、かつ 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;
// エラー、エディターがエラーを表示しない場合は、strictモードを有効にしてください。なぜなら、後で双方向共変について説明します
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
を設定するか、strict モードを無効にすることで回避できます。この場合、親タイプを子タイプに代入でき、子タイプを親タイプに代入できるようになります。このような場合、双方向共変と呼びます。
ただし、これは明らかに問題があり、型の安全性を保証できません。したがって、通常は strict モードを有効にして、双方向共変を回避します。
不変#
不変は最も単純です。継承関係がない場合(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">;