TypeScript 类型父子级、逆变、协变、双向协变和不变

我也是最近刚接触到了这些知识，文章可能有些错误，希望大佬多多指点（

对于学习 TypeScript 了解类型的逆变、协变、双向协变和不变是很重要的，但你只要明白类型的父子级关系，这些概念理解起来就会容易许多，因此在讲述这些之前我们必须先学会类型的父子级关系。

类型的父子级

首先明确一个概念，对于 TypeScript 而言，只要类型结构上是一致的，那么就可以确定父子关系，这点与 Java 是不一样的（Java 必须通过 extends 才算继承）。

我们可以看下面的例子:

Copy
interface Person {
name: string;
age: number;
}

interface Suemor {
name: string;
age: number;
hobbies: string[]
}

你应该可以发现这两个类型是有继承关系，此时你可以去思考到底谁是父级、谁是子级？

你可能会觉得 Suemor 是 Person 的父类型（毕竟 Person 有 2 个属性，而 Suemor 有 3 个属性且包含 Person），如果是这么理解的话那就错。

在类型系统中，属性更多的类型是子类型，也就是说 Suemor 是 Person 的子类型。

因为这是反直觉的，你可能很难理解（我当时也理解不了），你可以尝试这样去理解：因为 A extends B , 于是 A 就可以去扩展 B 的属性，那么 A 的属性往往会比 B 更多，因此 A 就是子类型。或者你记住一个特征，子类型比父类型更加具体。

另外判断联合类型父子关系的时候， 'a' | 'b' 和 'a' | 'b' | 'c' 哪个更具体？

'a' | 'b' 更具体，所以 'a' | 'b' 是 'a' | 'b' | 'c' 的子类型。

协变

对象中运用

协变理解起来很简单，你可能在平日里开发经常用到，例如:

Copy
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，也就是子级可以赋值给父级，反之不行（至于为什么，你可以想想假如 person 能够正确赋值给 suemor，那么调用 suemor.hobbies你的程序就坏到了）。

因此得出结论: 子类型可以赋值给父类型的情况就叫做协变。

函数中运用

同样的函数中也可以用到协变，例如:

Copy
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 类型的时候，可以这样去写:

Copy
interface Action {
type: string;
}

function dispatch<T extends Action>(action: T) {

}

dispatch({
type: "suemor",
text:'测试'
});

这样约束了传入的参数一定是 Action 的子类型，也就是说必须有 type，其他的属性有没有都可以。

双向协变

我们再看一下上上节的例子:

Copy
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 没有一方包含对方全部属性）那它就是不变，因此非父子类型之间只要类型不一样就会报错:

Copy
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;

逆变

逆变相对难理解一点，看下方例子:

Copy
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，那就坏掉了。

因此我感觉逆变一般会出现在：父函数参数与子函数参数之间赋值的时候（注意是函数与函数之间，而不是调用函数的时候，我是这么理解的，不知道对不对）。

因为逆变相对在类型做运算时用的会多一点，因此我们再看一个稍微难一点例子:

Copy
// 提取返回值类型
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，如下为正确答案:

Copy
type GetReturnType<Func extends Function> = Func extends (
...args: any[]
) => infer ReturnType
? ReturnType
: never;

type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;