看以下代码
type all2 = { a: "a" } & { b: "b" };
let all2: all2 = {
a: "a",
b: "b",
};
all2 类型是一个交叉类型 { a: "a" } & { b: "b" },其值与类型对应的得上,没毛病
但是再看这段代码
type all = { a: "a" } | { b: "b" };
let all: all = {
a: "a",
b: "b",
};
诶?为什么明明 all 的值只能是类型 { a: "a" } 或者 { b: "b" },但是其实际值的类型是 { a: "a" } & { b: "b" } 却又通过了校验呢?
这实际上正是联合类型与逆变之间的默契所在!
实际上,协变与逆变可以概括为:当我们想把一个 Y 类型的值赋给 X 类型的值时,只有 Y 类型的值不打破之前代码对 X 类型的值的使用,才能够进行赋值
看以下代码
type funcC = (k: { a: "a" } & { b: "b" }) => void;
let funcC: funcC = (k) => {
console.log(k.a, k.b);
};
type funcA = (k: { a: "a" }) => void;
let funcA: funcA = (k) => {
console.log(k.a);
};
type funcB = (k: { b: "b" }) => void;
let funcB: funcB = (k) => {
console.log(k.b);
};
funcC(all2);
funcC = funcA;
funcC(all2);
funcC = funcB;
funcC(all2);
在 funcC = funcA 之后,funcC 所接收的参数类型,实际上应该是 funcA 的参数类型,即 { a: "a" },但是
我们真正给到的参数类型却是 { a: "a" } & { b: "b" },然而这里并没有报错,因为即便你传入的值包含了 b 字段,也不会影响到此时函数 funcC (其值实际上是 funcA)中对 a 值的使用,更不会影响到funcC = funcA之后对函数 funcC 的使用,哪怕你多传了字段 b,因此赋值合理,这实际上也就是逆变。
但是,如果函数参数这块使用的是协变,那么等式 funcA = funcC 将成立,然而当你把鼠标放在 funcA 上时,其参数的类型还是{ a: "a" },如果是其他人不明所以的使用这个函数时,只传递了类型为{ a: "a" }的值,但是程序却报错说缺少 b 字段时,别人可能会感到很疑惑,因此,函数参数这里只能使用逆变,而为了达成逆变就必须让类型为{ a: "a" } & { b: "b" } 赋值给类型 { a: "a" } 的时候程序不报错,funcC = funcB同理。
在funcC = funcA之后,当你把鼠标悬浮在 funcC 上时,提示的类型仍旧是 (k: { a: "a" } & { b: "b" }) => void,然而在类型校验时,实际上是看类型 { a: "a" } & { b: "b" } 的值能否赋给类型 { a: "a" }, 如果这个时候类型校验报错了,但是实际运行又不报错,这个时候很令人匪夷所思,所以就必须类型校验不报错,所以联合类型在这里让步给了逆变,也是静态类型校验系统给实际运行时的让步