learn-typescript

类型兼容性

介绍

TypeScript 里的类型兼容性是基于 结构子类型 的,是根据 JavaScript 代码的典型写法来设计的。
结构类型 是一种只使用其成员来描述类型的方式,与 名义(nominal)类型 形成对比。【在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。】

interface Named {
  name: string
}

class Person {
  name: string = 'person'
}

let p: Named
p = new Person()

这段代码在基于 名义类型 的语言中(如 C#, Java)会报错,因为 Person 类没有明确说明实现了 Named 接口,但是在 TypeScript 中是正确的。

对象兼容性判断

TypeScript 的结构类型系统的基本规则是, 如果 x 要兼容 y ,那么 y 必须至少具有 x 中所有必须的属性:

interface Named {
  name: string
  gender?: string
}

let x: Named
let y = { name: 'alice', age: 12}
x = y  // ok

检查函数的参数时使用相同的规则:

function greet(n: Named){
  console.log('Hello, ', n.name)
}

greet(y)  // ok
greet({ name: 'bob', age: 12}) // 错误,注意:直接传入对象字面量会进行 `额外的属性检查`

函数兼容性判断

1.参数列表类型比较

参数列表的比较只关心对应位置的参数类型,与名称无关。目标函数 y 的参数列表必须是源函数 x 的参数列表的父集合【非必须参数除外】:

let x = (a: number) => 0
let y = (a: number, b: string) => 0

y = x // ok
x = y // Error

2.返回值类型比较

目标函数 y 的返回值类型必须为源函数 x 的返回值类型的子集合:

let x = () => ({ name: 'x', age: 12 })
let y = () => ({ name: 'y' })

y = x // ok
x = y

3.可选参数和剩余参数

可选参数/默认参数

在比较兼容性时:

  1. 源函数有额外的可选参数不是错误:
    ```ts let x = (a: string) => 0 let y = (x: string, y?: boolean) => 0

x = y


2. 目标函数上有额外的可选参数也不是错误:  
  ```ts
  let x = (a: string, b?: number) => 0
  let y = (x: string) => 0

  x = y

剩余参数

当一个函数有剩余参数时,剩余参数被当作无数个可选参数。

4.有重载的函数的兼容性判断

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。

枚举兼容性

枚举类型和数字类型相互兼容:

enum Enum {
  A,
  B
}

let enumB: number = Enum.B
let enumA: Enum = 0

类兼容性

类进行兼容性判断时,只会对实例成员进行比较:

class Animal {
  name: string
  constructor(name: string) {
    this.name = name
  }
}

class Person {
  name: string
  constructor(name: string, age: number) {
    this.name = name
  }
}

let a = new Animal('animal')
let p = new Person('person', 0)

a = p
p = a

类的私有成员/受保护成员

私有成员(或受保护成员)会影响兼容性判断:如果目标类型包含一个私有成员(或受保护成员),那么源类型必须包含来自同一个类的这个私有成员(或受保护成员)。
典型例子: 父类有私有属性,其子类可以赋给该父类,但是不可以赋给和父类有同样结构的其他类:

class Animal {
  name: string
  private id: number
  constructor() {
    this.id = 0
    this.name = 'animal'
  }
}

class Person extends Animal {
  constructor() {
    super()
    this.name = 'person'
  }
}

class Plant {
  name: string
  private id: number
  constructor() {
    this.name = 'plant'
    this.id = 1
  }
}

let a: Animal = new Person()  // ok
let pl: Plant = new Person()  // 错误,Plant类虽然和Animal类都有相同的private属性,但是Person类实例只能赋给其父类Animal