浅谈TS中的类型兼容性和This

·  阅读 524

类型兼容性

类型兼容性主要是通过赋值和传参来体现的,比如把一个number类型变量赋值给一个string肯定是不兼容的,那其他类型相互之间的兼容性如何呢?

基础类型

如下表所示:

  • any类型除了不能赋值给never与其他所有类型兼容
  • unknow可以被其他所有类型赋值
  • anyunknow外,void不可赋值给其他类型
  • 不开启--strictNullChecks选项时,void可被nullundefinednever赋值
  • 开启--strictNullChecks选项时,void可被undefinednever赋值

基础类型兼容性

对象

TypeScript使用结构化的类型检查系统,即判断两个类型是否兼容,只需要判断他们的“结构”是否一致,也就是结构属性名和类型是否一致。

两个拥有相同结构的类型,子类型可以赋值给父类型,反之报错。

interface Animal {
  age: number
}

interface Dog extends Animal {
  bark(): void
}

declare let animal: Animal
declare let dog: Dog

animal = dog 
dog = animal // error: Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
复制代码

如上子类型dog可以赋值给父类型,反之报错。这样的设计是处于类型安全的考虑。animal这个变量只有age属性,被使用时也只能访问或修改其age属性,而dog子类型也拥有age属性,所以赋值给animal后,不会影响原有逻辑。反之,dog上有一些animal不存在的属性,如果用户对这些属性做了操作,将animal赋值给dog就会报错,这是类型不安全的。

数组同理

declare let animals: Animal[]
declare let dogs: Dog[]

animals = dogs

dogs = animals // Property 'bark' is missing in type 'Animal' but required in type 'Dog'
复制代码

函数赋值

declare let visitAnimal: (animal: Animal) => void

declare let visitDog: (dog: Dog) => void

visitDog = visitAnimal

visitAnimal = visitDog //  Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
复制代码

如上,发现参数类型是子类型的赋值给参数类型是父类型的发生了报错。

之所以这样设计是处于类型安全的角度考虑,试想如下实现

let visitAnimal = (animal: Animal) => {
  animal.age
}

let visitDog = (dog: Dog) => {
  dog.age
  dog.bark()
}
复制代码

如果将visitAnimal赋值为visitDog(visitAnimal = visitDog) ,我们依然期望传递一个Animal类型的参数给visitAnimal,而这个参数可能是不带bark属性的,这样就会报错。

visitAnimal = visitDog

let animal = { age: 5 }

visitAnimal(animal) // 报错
复制代码

反之,如果将visitDog赋值为visitAnimal(visitDog = visitAnimal),我们依然期望传递一个Dog类型的参数给visitDog,由于Dog类型是Animal的子类型,所以赋值后也不会报错,类型是兼容的。

visitDog = visitAnimal

let dog = { age: 5, bark: () => 'bark' }

visitDog(dog) // 不会报错
复制代码

此外,在 TypeScript 中,由于灵活性等权衡,对于函数参数默认的处理是双向协变 的。也就是既可以 visitAnimal = visitDog,也可以 visitDog = visitAnimal。在开启了 tsconfig 中的 --strictFunctionType 后才会严格按照如上例所示来约束赋值关系.

同样,也有如下情况。函数fun1的参数类型比fun2更具体,是其的子类型,所以fun1不能赋值给fun2。

declare let fun1: (value: any, extraData: any) => void

declare let fun2: (value: any) => void

fun1 = fun2

fun2 = fun1 // error: Type '(value: any, extraData: any) => void' is not assignable to type '(value: any) => void'.
复制代码

联合类型赋值

如下,对于联合类型而言'a' | 'b''a' | 'b' | 'c'更具体,所以前者是后者的子类型。

type Parent = 'a' | 'b' | 'c'
type Son = 'a' | 'b'

let parent: Parent
let son: Son

parent = son
son = parent // error: parent 有可能是 'c'

复制代码

枚举

对于枚举值,枚举值和number互相兼容,而不同枚举类型的枚举值不兼容。

enum Status {
  Ready,
  Waiting,
}
enum Color {
  Red,
  Blue,
  Green,
}

declare let num: number

let ready = Status.Ready;
ready = Color.Green; // Type 'Color.Green' is not assignable to type 'Status'

ready = num

num = ready
复制代码

总结

TS中,函数赋值,参数要求是逆变的,即父类型参数可以赋值给子类型参数,反之不可。其他情况都是子类型可赋值给父类型。

ThisType

对象调用

方法作为对象的属性时,其内部this会被自动推导

const obj1 = {
  name: "name",
  getNam() {
    return this.name // 可以自动推导为{ name:string, getName():string}类型
  },
}

// 使用箭头函数会报错
const obj1 = {
  name: "name",
  getNam: () => {
    return this.name // error: The containing arrow function captures the global value of 'this'
  },
}
复制代码

构造器调用

typescript不支持es5构造函数的类型推断,也就是以下写法会报错。

如果开启了--noImplicitThis选项,在es5的构造函数内使用this会报错;此外,也不能通过new操作符来调用。

建议直接使用es6的class语法,this会被正确推断

function People(name: string) {
  this.name = name // error: 'this' implicitly has type 'any' because it does not have a type annotation.
}

new People() // error: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
复制代码

函数调用或call、apply调用

如果通过callapply调用,this则是动态的值,Typescript支持显式声明this的类型

const person = {
  name: "name",
  getNam() {
    return this.name
  },
}

function setName(this: typeof person, name: string) {
  this.name = name
}

setName.call(person, 'new name')
复制代码

如上示例,显式声明了setName函数内部this的类型

分类:
前端
标签: