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}) // 错误,注意:直接传入对象字面量会进行 `额外的属性检查`
参数列表的比较只关心对应位置的参数类型,与名称无关。目标函数 y
的参数列表必须是源函数 x
的参数列表的父集合【非必须参数除外】:
let x = (a: number) => 0
let y = (a: number, b: string) => 0
y = x // ok
x = y // Error
目标函数 y
的返回值类型必须为源函数 x
的返回值类型的子集合:
let x = () => ({ name: 'x', age: 12 })
let y = () => ({ name: 'y' })
y = x // ok
x = y
在比较兼容性时:
x = y
2. 目标函数上有额外的可选参数也不是错误:
```ts
let x = (a: string, b?: number) => 0
let y = (x: string) => 0
x = y
当一个函数有剩余参数时,剩余参数被当作无数个可选参数。
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。
枚举类型和数字类型相互兼容:
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