协变、逆变、双向协变
简单解释就是 假如 类型A 是 类型B 的子类型(类型系统中,类型属性更具体的是子类型,属性更少则说明该类型约束的更宽泛,称为父类型)
- 协变:指 A 可以赋值给 B
- 逆变:与协变相反, 指 B 可以赋值给 A
- 双向协变:指既可以协变也可以逆变
# 协变
例子
class Animal {
name: string | undefined
}
class Bird extends Animal {
fly(){}
}
let val: Animal = new Animal()
val = new Bird() // ok 协变
2
3
4
5
6
7
8
9
10
在比较两个函数时,先不考虑参数的情况下,类型系统规定源函数的返回值类型必须是被赋值函数返回值类型的子类型,即协变。
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error
let fun1 = ()=> Animal
let fun2 = ()=> Bird
fun1 = fun2 // ok 子类型可以赋值给父类型
fun2 = fun1 // Error 父类型不可以赋值给子类型
2
3
4
5
6
7
8
9
10
11
12
# 逆变、双向协变
逆变和双变只能针对函数的赋值
在默认情况下 strictFunctionTypes
设置为 false
,也就是支持函数参数双向协变的情况下
// --strictFunctionTypes : fasle
class Animal {
name: string | undefined
}
class Bird extends Animal {
fly(){}
}
let fun1: (x: Animal) => void = (x: Animal)=>{}
let fun2: (x: Bird) => void = (x: Bird)=>{}
fun1 = fun2 // ok
fun2 = fun1 // ok
2
3
4
5
6
7
8
9
10
11
12
13
14
开启了 strictFunctionTypes
设置为 true
,则
// --strictFunctionTypes : true
fun1 = fun2 // error
fun2 = fun1 // ok
2
3
在不考虑函数返回值类型的情况下:
- 在 fun1 = fun2 中,也就是 fun2(参数类型 Bird ) 赋值 给 fun1(参数类型 Animal),这是协变
- 在 fun2 = fun1 中,也就是 fun1(参数类型 Animal ) 赋值 给 fun2(参数类型 Bird ),相反,这是逆变 可以发现,开启 strictFunctionTypes 后,参数逆变是支持的,协变错误了
原因举例
class Animal {
name: string | undefined
}
class Bird extends Animal {
fly(){}
}
class Fish extends Animal {
swim(){}
}
let fun1: (x: Animal) => void = (x: Animal)=>{
console.log(x.name)
}
let fun2: (x: Bird) => void = (x: Bird)=>{
x.fly()
}
fun1 = fun2
let myFish = new Fish()
fun1(myFish) // 运行程序报错,x.fly is not a function
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在上面的例子中,在函数参数类型支持协变的情况下,我们将 fun2 赋值给 fun1 ,即 fun1 变成 了 (x)=>{x.fly()},但是fun1的类型还是(x: Animal) => void,所以我们可以传入 new Fish() 作为参数,这时候运行函数由于 Fish 上没有 fly 方法,所以程序执行报错了。 所以在日常开发中,最好开启严格模式,禁用函数参数双向协变检查。
# 变量赋值检查
有变量 a 和 变量 b,如果 b 要赋值给 a,那么需要检查 a 的类型的属性是否在 b 类型中都有对应的属性
let a: { name: string } = { name: "jack" };
let b: { name: string; age: number; gender: string } = {
name: "jack",
age: 19,
gender: "man",
};
a = b; // ok b类型有a
b = a; // error 类型a 缺少类型b 中的以下属性: age, gender
2
3
4
5
6
7
8
函数参数的赋值检查和变量赋值检查同理
function fun(user: { name: string }) {}
let myUser = {
name: "jack",
age: 19,
gender: "man",
};
fun(myUser); // ok
2
3
4
5
6
7
注意,在上面例子中,如果如下调用函数会报错
fun({
name: "jack",
age: 19,
gender: "man",
}); // error
2
3
4
5
如果用上面的写法,在参数中写对象相当于直接给 user 参数赋值,这时候会遵循严格的类型定义,但如果先用一个变量接收(例如上面的myUser),就可以不经过额外属性检查。因为这时候相当于把 myUser 这个变量赋值给 user,根据上面的 变量赋值类型兼容性,可以赋值成功,这样就可以绕开额外类型检查。
# 函数参数个数比较
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error `y`有个必需的第二个参数,但是`x`并没有,所以不允许赋值。
2
3
4
5
两个函数x和y,如果函数x要赋值给函数y,函数x的每个参数都要在函数y中可以找到对应类型的参数。 这是由于在 js 中,额外的参数可以忽略不传。 例如
let x = (a: number):void => {
console.log(a)
};
let y = (b: number, s: string):void => {
console.log(b+s)
};
y = x // ok
y(1, 'a') // 这时候调用,因为函数y已经被赋值为 (a: number) => void; 但是其类型还是(b: number, s: string) => void,所以还是要传两个参数,但是调用是只会用到第一个参数,这时候程序可以正常执行,打印结果是-1
2
3
4
5
6
7
8
如果 y 可以赋值给 x,这时候根据 x 的类型 x 只能传入一个参数,而 y 需要两个参数,这时候运行函数就会报错
- 01
- 2023/07/03 00:00:00
- 02
- 2023/04/22 00:00:00
- 03
- 2023/02/16 00:00:00