“这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战”
TS 的 类型兼容是基于 结构化 的子类型。结构化的类型是一种完全基于其类型成员的关系的类型。
以下方为例:
interface Pet{
name: string;
}
class Dog{
name: string
}
let pet: Pet;
// OK, because of structual typing
pet = new Dog;
在 C# 或 Java 这种 名义类型 语言上,相同的代码会报错。因为 Dog 类并没有实现 Pet 这个界面。
TS 结构化类型系统的设计是基于 JS 代码的书写。因为 JS 广泛使用匿名对象,如 函数表达式 和 字面量对象。因此,使用一个结构化类型系统代替字面意义上的类型系统,去表达 JS 类库之间的各种关系是一个更为自然的方式。
健全:A Note on Soundness
TS 的类型系统是不健全的。在其编译期间一些无法被类型系统感知的确定的操作是安全的。当一个类型系统有如上特点,那么其就是不健全的。TS 允许不健全的行为是经过完备考虑的,并且通过这个文档,会阐述不健全行为是在哪发生的且其背后的动机。
Starting out
TS 结构化类型系统的基本规则是:如果 y 的类型成员包含 x 的类型成员,那么 x 与 y 的类型是兼容的。
interface Pet{
name: string;
}
let pet: Pet;
let dog ={ name: "name", owner: "owner"}
// pet: Pet
// dog : { name:string, owner: string}
pet = dog
检查 dog 是否可以被赋值给 pet,编译器会检查 pet 的属性,并且在 dog 的类型中找到其兼容的属性。在这个案例中,dog 必须有一个名为 name 的属性,其类型为 string。事实上 dog 确实满足这个条件,因此两个类型是兼容的。
这个规则也适用于检查函数的参数类型。
interface Pet {
name: string
}
let dog = { name: "name", owner: "owner"}
function greet(pet:Pet){
console.log(pet.name)
}
greet(dog)
注意:dog 中有额外的 owner 属性,但是并没有报错。在检查类型兼容的时候,TS 只会考虑目标类型的类型成员。
检查每个类型成员及其子成员的时候,这个检查过程是递归进行的。
比较两个函数
相比于比较原始类型与对象类型的类型兼容性,比较函数之间的类型兼容是更加难。
首先,从下面的简单例子开始,二者仅有参数类型不同
let x = (a:number) => 0
let y = (b:number, s:string) => 0
y = x ; // OK
x = y ; // Error
检查 x 是否可以赋值给 y,首先要检查二者的参数。x 中的每个参数,应该在 y 中能够找到兼容的类型。注意,只关系参数的值的类型,而并非参数名称。在这个案例中,x 中的每个参数的值的类型都可以在 y 中找到其对应的参数的值的类型。
第二个赋值出错是因为,y 有两个参数,但是 x 仅有一个。因此,赋值不成立。
你可能会疑惑,为什么我们允许第一个赋值式的行为。因为,这种行为在 JS 中是正常现象。以数组函数 forEach 为例,在使用的是经常会忽略 index 参数。
接下来比较一下返回值
let x = () => ({ name: "Alice"})
let y = () => ({ name: "Alice", location: "Seatttle"})
x = y; // OK
y = x; // Error
类型系统强制源函数的返回类型是目标函数返回类型的子类型。
函数参数的双向协变
当比较函数参数的类型时,如果源参数可以可以赋值给目标参数,或者目标函数参数可以赋值给源参数,那么分配就是成功的。这个是类型不健全的,因为调用者会给予函数一个更具体的类型,但是调用这个函数会给予一个不太具体的类型。实际上,这类错误是很少见的。
enum EventType{
Mouse,
Keyboard
}
interface Event {
timestamp: number
}
interface MyMouseEvent extends Event{
x: number;
y: number;
}
interfeace MyKeyEvent extends Event {
keyCode: number;
}
function listenEvent(eventType: EventType, handler: (n: Event) => void){
// ...
}
// 类型不健全,但是 常见 且 有用
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + e.y));
// 考虑到健壮性,不期望的用法
listenEvent(EventType.Mouse, (e: Event) => console.log((e as MyMouseEvent).x))
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x) as (e:Event => void))
// 明显的错误,类型不兼容
listenEvent(EventType.Mouse, (e:number) => console.log(e))
可选参数与剩余参数
当比较函数兼容性的时候,可选参数与剩余参数是可以交换的。源类型的额外可选参数并不是一个错误;目标类型的可选参数在对应源类型中并不存在也不是一个错误。
当一个函数有一个剩余参数,那么其会被作为一个有限的可选参数。
这个行为在类型系统中是不健全的。但是从运行时角度来看,可选参数并不是强制的 ,因为会传入 undefined 来代替。
function invokeLater(args:any[], callback:(...args:any[]) => void){
//
}
// 不健全,
invokeLater([1, 2], (x, y) => console.log(x, y))
// 迷惑行为大赏,x y 实际上是需要的,并且难以发现
invokeLater([1, 2], (x?, y?) => console.log(x, y))
函数重载
函数重载时,每一个源类型必须匹配目标类型。这个保证了目标函数可以在所有相同情况下作为源函数被调用。
枚举
枚举类型与数子类型兼容,但是数组类型与枚举类型不兼容。来自不同类型的枚举值是不兼容的。
enum Status{
Ready,
Waiting
}
enum Color {
Red,
Blue,
Green
}
let status = Staus.Ready
// Error
status = Color.Green
类
类的兼容性与对象字面量大体上与界面相似。但是有一处不同:类同时具有静态类型与实例类型。当比较两个具有类类型的对象时候,只比较其实例类型的成员。静态成员与构造函数并不影响兼容性。
class Animal{}
class Size{}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
类中的私有成员与保护成员
类中的私有成员与保护成员影响类的兼容性。当检测类实例的兼容性时,如果目标类型有一个私有属性,那么源类型也必须有一个源于相同类型的私有属性。同样地,也适用于保护成员。这是得类与其超类具有兼容性。
class A { private a: string; protected b: string; constructor(a: string, b:string){ this.a = a; this.b = b } }class B { private a: string; protected b: string; constructor(a: string, b:string){ this.a = a; this.b = b } }class C { public a:string; constructor(a: string){ this.a = a; } }
// error
let a:A = {a:"a", b:"b"}// error
let b:B = {a:"a", b:"b"}
// work
let c:C = {a:"a"}
// error
let a = b
泛型
当泛型别辅助一个特性值的是,其兼容判定与一个非泛型类型相同。
interface Empty<T>{}
let x: Empty<number>{}
let y: Empty<string>{}
// work
x = y
interface NotEmpty<T>{
data: T
}
let x: Empty<number>{}
let y: Empty<string>{}
// error
x = y
这里,类型是兼容的,因为没有泛型 T U 的具体值是,会被赋予 any 类型。那么这两个函数是兼容的。
let identity = function <T>(x: T): T { // ...};let reverse = function <U>(y: U): U { // ...};identity = reverse; // OK, because (x: any) => any matches (y: any) => any
进阶
子类型与赋值
到目前为止,我们使用了兼容性一词。这并不是一个编程语言维度的标准。在 TS 中,有两种兼容性:子类型 与 赋值。二者区别仅在于 赋值 扩展了子类型的兼容性(any 类型的赋值,枚举类型与数字值的互相赋值)
下图为基础类型的类型兼容性,其中绿色的 √ 为关闭 --strictNullCheck 时成立。
1. 任何类型可以赋值给自身
2. 任何类型都可以分配给 any 与 unknown。但是 unknown 只可以分配给any。
3. never 与 unknown 类型相反。任何值可以分配给unknown,never可以分配给任何值。没有类型可以分配给never,除了any,unknown不可以分配给其他类型
4. 除了 any、unknown、never、undefined、null ( --strictNullChecks 关闭)以外,void 不可以分配给其他类型。
5. --strictNullChecks 关闭,null 和 undefined 与 never 类似:可以分配给大部分类型;大部分类型不可以分配给他们。他们可以互相分配。
6. --strictNullChecks 开启,null 和 undefined 与 void 类似:除 any、unknown、never、void之外,不可以与任意类型分配、被分配。