TypeScript 类型父子级、逆变、协变、双向协变和不变
我也是最近刚接触到了这些知识,文章可能有些错误,希望大佬多多指点(
对于学习 TypeScript 了解类型的逆变、协变、双向协变和不变是很重要的,但你只要明白类型的父子级关系,这些概念理解起来就会容易许多,因此在讲述这些之前我们必须先学会类型的父子级关系。
类型的父子级
首先明确一个概念,对于 TypeScript 而言,只要类型结构上是一致的,那么就可以确定父子关系,这点与 Java 是不一样的(Java 必须通过 extends 才算继承)。
我们可以看下面的例子:
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' 的子类型。
协变
对象中运用
协变理解起来很简单,你可能在平日里开发经常用到,例如:
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
你的程序就坏到了)。
因此得出结论: 子类型可以赋值给父类型的情况就叫做协变。
函数中运用
同样的函数中也可以用到协变,例如:
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">;