TS类型兼容性

174 阅读4分钟

类型介绍

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类型

返回值兼容

上面讲了逆变和协变,这个就是典型的协变了。对于返回值来说,只要满足子类型赋值给父类型就好了