跳到主要内容

协变与逆变(针对函数)

子类型

在了解协变与逆变之前我们需要知道一个概念——子类型。我们前面提到过 string 可以赋值给 unknown 那么就可以理解为 string 是 unknown 的子类型。正常情况下这个关系即子类型可以赋值给父类型是不会改变的我们称之为协变,但是在某种情况下两者会出现颠倒我们称这种关系为逆变。如:

interface Animal {
name: string;
}

interface Cat extends Animal {
catBark: string;
}

interface OrangeCat extends Cat {
color: "orange";
}

// ts 中不一定要使用继承关系,只要是 A 的类型在 B 中全部都有,且 B 比 A 还要多一些类型
// 类似集合 A 属于 B 一样,这样就可以将 B 叫做 A 的子类型。

// 以上从属关系
// OrangeCat 是 Cat 的子类型
// Cat 是 Animal 的子类型
// 同理 OrangeCat 也是 Animal 的子类型

const cat: Cat = {
name: "猫猫",
catBark: "喵~~",
};
const animal: Animal = cat; // no error

实际

逆变(通常在参数中体现):父类型的位置用更具体的子类型代替

type A = { name: string };
type B = { name: string, age: number };

// 逆变:将具有更多属性的类型B赋值给类型A
let fn: (arg: A) => void;
fn = (arg: B) => {
console.log(arg.name);
};

协变(通常在返回中体现):子类型的位置用更抽象的父类型代替

type C = { name: string };
type D = { name: string, age: number };

// 协变:将类型C的返回值赋给具有更少属性的类型D
let fn: () => D;
fn = () => {
return { name: "John" };
};

逆变和协变可以增加类型的灵活性和兼容性,但是可能导致类型不安全或运行时错误

假设我有类型 FnCat 请问下面四个谁是它的子类型,即以下那个类型可以赋值给它。


type FnCat = (value: Cat) => Cat;

type FnAnimal = (value: Animal) => Animal;
type FnOrangeCat = (value: OrangeCat) => OrangeCat;

// 入参变抽象了,返回值变具体了
type FnAnimalOrangeCat = (value: Animal) => OrangeCat;
type FnOrangeCatAnima = (value: OrangeCat) => Animal;

type RES1 = FnAnimal extends FnCat ? true : false; // false
type RES2 = FnOrangeCat extends FnCat ? true : false; // false
type RES3 = FnAnimalOrangeCat extends FnCat ? true : false; // true
type RES4 = FnOrangeCatAnima extends FnCat ? true : false; // false

为什么 RES3 是可以的呐? 返回值:假设使用了 FnCat 返回值的 cat.catBark 属性,如果返回值是 Animal 则不会有这个属性,会导致调用出错。估计返回值只能是 OrangeCat。 参数:假设传入的函数中使用了 orangeCat.color 但是,对外的类型参数还是 Cat 没有 color 属性,就会导致该函数运行时内部报错。 故可以得出结论:

返回值只支持协变,入参只支持逆变

(返回值不能变抽象但可以更详细,入参不能变详细但可以变抽象,才能作为原函数的子类型)

注意如果 tsconfig.json 中的 strictFunctionTypes 是 false 则上述的 RES2 也是 true ,这就表明当前函数是支持双向协变的(入参可以变详细)。当然 TS 默认是关闭此选项的,主要是为了方便 JS 代码快速迁移到 TS 中,详情可以见 why-are-function-parameters-bivariant ,当然如果是一个新项目,建议打开 strictFunctionTypes 选项。 允许双向协变是有风险的,可能会在运行时报错。比如在 ESLint 中有 method-signature-style 规则,简单的来说该规则默认是使用 property 来声明方法,比如: 假设我们忽略 eslint 警告,强制 T2 的方法进行声明就会潜在的双向协变的风险

example

在不开 strictfunctionTypes 情况下,入参只支持逆变不支持协变

可以用这种 hack 法解决

class Animal {
private x: undefined
}

class Dog extends Animal {
private d: undefined
}
type EventHandler<E extends Animal>= (event: E) => void
let z: EventHandler<Animal>= (o: Dog) => {} // fails under strictfunctionTypes
type BivariantEventHandler<E extends Animal>= {
bivarianceHack(event: E): void
}['bivarianceHack']

let y: BivariantEventHandler<Animal>= (o: Dog) => {}