类型介绍
TypeScript里的类型兼容性是基于结构子类型的,结构类型是一种只使用其成员来描述类型的方式,也就是俗称的鸭子模型。如果两个类型拥有相同的结构就认为他们是同一类型
class Point {
x: number = 1
y: number = 2
}
class Point2d {
x: number = 3
y: number = 4
}
type PointType = {
x: number
y: number
}
let point: PointType = new Point()
point = new Point2d()
console.log(point)
类型兼容
在集合中我们知道,如果集合A的所有元素都在集合B中,那么A是B的子集。
TS的类型系统同样可以使用这个概念,如果A类型可以描述的元素集合都在B类型描述的元素集合中,那么A就是B的子类型。
class Point {
x: number = 1
y: number = 2
}
class Point3d {
x: number = 3
y: number = 4
z: number = 5
}
上面的例子可以简单理解一下,Point需要包含x,y属性;Point3d除了x,y还需要包含z属性;对于他们描述的属性集合来说,显然Point描述的元素范围更广,所以Point3d是Point的子类
从上面的例子我们可以得出一个结论:如果一个类型的属性更具体,则该类型是子类型,因为属性更少说明约束更宽泛,子类型比父类型更加具体, 父类型比子类型更宽泛
一般情况下子类型的值可以赋值给父类型。特殊情况比如在函数赋值兼容时会有一些变化,这个稍后讲
class Point {
x: number = 1
y: number = 2
}
class Point3d {
x: number = 3
y: number = 4
z: number = 5
}
// 这里是兼容的,可以正常赋值
let point: Point = new Point3d()
// 这样就不能正常赋值了
let point3d: Point3d = new Point()
这里还需要注意一点,当我们执行let point: Point = new Point3d(),直接打印point会发现实际上point值为{x: 3, y: 4, z: 5},但是当我们使用point.z的时候编译时发现仍然会报错,所以不管值是什么都会受原本的类型约束
函数兼容性-逆变与协变
参数类型兼容,逆变与协变介绍
这个一直是ts中很难理解的一部分
协变与逆变(Covariance and contravariance )是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变的,而关系逆转了(子变父,父变子)就是逆变的
看看下面的例子
协变
class Animal {}
class Dog extends Animal {
bark(): void {
console.log("Bark")
}
}
class Greyhound extends Dog {
fur: string = 'Grey'
}
function makeDogBark(dog: Dog) : void {
dog.bark()
}
let animal: Animal = new Animal();
let dog: Dog = new Dog();
let greyhound: Greyhound = new Greyhound();
makeDogBark(greyhound) // OK。 子类赋值给父类
makeDogBark(animal) // Error
逆变
class Animal {}
class Dog extends Animal {
bark(): void {
console.log("Bark")
}
}
class Greyhound extends Dog {
fur: string = 'Grey'
}
let makeDogBark: (dog: Dog) => void = (dog) => {
dog.bark()
}
let makeGreyhoundBark: (greyhound: Greyhound) => void = (greyhound) => {
greyhound.bark()
console.log(greyhound.fur)
}
makeGreyhoundBark = makeDogBark
// Error
// makeDogBark = makeGreyhoundBark
协变其实很好理解,其实就是简单的赋值,子类型可以赋值跟父类型。这里一定要把,函数调用和函数赋值这俩概念区分开,不然就会比较绕。
逆变就会很反直觉,在我们上面的例子中,第一感觉就是makeDogBark参数是dog所以是父类型,而makeGreyhoundBark参数是Greyhound所以是子类型,但是赋值方式却是相反的
这里可以怎么理解呢,根据我们上面的函数实现可以知道。子类型函数实现功能会更复杂,用到的属性会更多(比父类型多);而父类型用的属性更少。
假设我们把子类型的方法makeGreyhoundBark赋值给父类型makeDogBark
let makeDogBark: (dog: Dog) => void = (greyhound) => {
greyhound.bark()
console.log(greyhound.fur)
}
实际上就变成了下面这样,Dog类型中是没有fur属性的,显然这种赋值是不安全的
现在看看把父类型赋值给子类型,可以发现Greyhound完全满足父类型方法体的参数要求,所以这种赋值没有问题,只是会少了一些功能
let makeGreyhoundBark: (greyhound: Greyhound) => void = (dog) => {
dog.bark()
}
参数个数兼容
先来看看一个例子
type F1 = (a: number) => void
type F2 = (a: number, b:number) => void
let f1: F1 = (a) => {
console.log(a)
}
let f2: F2 = (a, b) => {
console.log(a, b)
}
f2 = f1
// 这个就会报错
f1 = f2
参数少的可以赋值给参数多的。
这个可以怎么理解呢,其实也能按照上面方法实现的方式理解,参数多的实现的功能就更复杂。参数少的方法实现用到的参数肯定包含在参数多的类型中
其实我觉得协变还可以参考TS类型系统中的联合类型,F1方法体中需要用到的参数类型是'a',F2方法体中需要用到的参数是'a' | 'b',那么'a' 是 'a' | 'b'的子类型,所以F1类型的值可以赋值给F2类型
返回值兼容
上面讲了逆变和协变,这个就是典型的协变了。对于返回值来说,只要满足子类型赋值给父类型就好了