TypeScript Type Compatibility
TS 允许将一些不同类型的变量相互赋值,虽然某种程度上可能会产生一些不可靠的行为,但增加了语言的灵活性,毕竟它的祖宗 JS 就是靠灵活性起家的,而灵活性已经深入前端的骨髓,有时会 不得不做一些妥协。
当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y。
X (目标类型) = Y (源类型)
Primitive type compatibility
原始类型的兼容性主要关注三点:
null/undefined
strict 模式下 null/undefined
不可以赋值给任意其他类型,但可以被其他任意类型赋值:
let str: string = 'str';
let nl = null;
str = nl; // Error: Type 'null' is not assignable to type 'string'.
nl = str; // OK
如果想避免以上错误,可以关闭以下选项:
{
"strictNullChecks": false
}
此时 s
是可以被赋值为 null
的,我们就可以说 null
是 string
的子类型:
let str: string = 'str';
let vd: void;
str = null; // OK
vd = null; // OK
vd = undefined; // OK
但是不可以赋值给 never
:
let nev: never;
nev = null; // Error: Type 'null' is not assignable to type 'never'.
any
any
类型可以赋值给任意其他类型(never
类型除外),也可以被其他任意类型赋值:
let ay: any = 'str';
let num: number;
num = ay; // OK
ay = num; // OK
ay = vd; // OK
never
任何类型都不能赋值给 never
:
let nev: never;
let nl = null;
nev = num; // Error
nev = vd; // Error
nev = ay; // Error
nev = nl; // Error
但在非 strict 模式下 never 可以赋值给任意值:
ay = nev; // OK
vd = nev; // OK
num = nev; // OK
nl = nev; // OK
以上是最基本的类型兼容,类型兼容还广泛应用于 enum, interface, class, function, generic 等。
Enum
Enum 的规则比较简单,我们先定义两个 enum:
enum Fruit {
Apple,
Banana,
}
enum Color {
Red,
Yellow,
}
enum 和 number 是可以完全兼容的:
let fruit: Fruit.Apple = 3; // OK
let no: number = Fruit.Apple; // OK
enum 之间是完全不兼容的:
let color: Color.Red = Fruit.Apple; // Error: Type 'Fruit.Apple' is not assignable to type 'Color.Red'.
Interface
后面的讨论我们会减少使用目标类型,源类型,谁兼容谁,这些术语,因为它们比较绕,可能过一会你就忘记了谁是谁。我们先想想怎么用图去表示:
![eSIaVD](https://cosmos-x.oss-cn-hangzhou.aliyuncs.com/eSIaVD.png)
不同大小的圆圈代表不同的类型或者成员的多少,先记住以上图的轮廓,我们再来看一个最简单的 例子:
interface X {
a: any;
b: any;
}
interface Y {
a: any;
b: any;
c: any;
}
let x: X = { a: 1, b: 2 };
let y: Y = { a: 1, b: 2, c: 3 };
x = y; // OK
y = x; // Error: Property 'c' is missing in type 'X' but required in type 'Y'.
从以上例子可以看出 y
可以赋值给 x
,但 x
不能赋值给 y
。其实就是说 y
只要具备 a
和 b
,就可以赋值给 x
类型。
TypeScript 类型兼容性是基于 Structural Typing 。它是一种仅根据成员来关联类型的方式,而不是由继承自特定的类或实现特定的接口决定,这与 C# 或 Java 的 Nominal Typing 不太一样。
究其原因,TypeScript 类型系统的设计是也基于 JavaScript 代码通常是如何编写的。由于 JS 广泛使用函数表达式和对象字面量等匿名对象,因此利用 Structural 类型系统而不是 Nominal 类型系统可以更自然地表示 JS 中存在的各种类型间的关系。
其实本质上就是动态语言的类型检查原则:鸭式变形法,总结一下就是:多的可以赋值给少的。
不过还是有点绕,我们可以增强一下最一开始的图:
![GqKI1h](https://cosmos-x.oss-cn-hangzhou.aliyuncs.com/GqKI1h.png)
Class
Class 和 Interface 相似,只比较结构,本质上也是 Duck Typing。但是要注意的是静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那么他们实例就可以完全兼容。
class A {
constructor(p: number, q: number) {}
id: number = 1;
}
class B {
static s = 1;
constructor(p: number) {}
id: number = 2;
}
let aa = new A(1, 2);
let bb = new B(1);
aa = bb; // OK
bb = aa; // OK
如果含有私有成员,就会导致不能相互兼容:
class A {
constructor(p: number, q: number) {}
id: number = 1;
private name: string = '';
}
class B {
static s = 1;
constructor(p: number) {}
id: number = 2;
private name: string = '';
}
let aa = new A(1, 2);
let bb = new B(1);
aa = bb; // Error: Types have separate declarations of a private property 'name'.
bb = aa; // Error: Types have separate declarations of a private property 'name'.
但即使有私有成员,子类也是可以赋值给父类的:
class A {
constructor(p: number, q: number) {}
id: number = 1;
private name: string = '';
}
class B {
static s = 1;
constructor(p: number) {}
id: number = 2;
private name: string = '';
}
let aa = new A(1, 2);
let bb = new B(1);
class C extends A {
gender: string = '';
}
let cc = new C(1, 2);
aa = cc; // OK
cc = aa; // Error: Property 'gender' is missing in type 'A' but required in type 'C'.
如果子类中含有其它属性或方法,父类是不可以赋值给子类的,因为如果子类实例调用该方法会取不到而报错。
如果子类实际上并没有扩展父类,那么彼此之间其实是可以相互兼容的。
Class 的类型兼容其实也是 Duck Typing,需要注意的是静态成员和构造函数