类型兼容性用于确定一个类型是否能赋值给其他类型。
如 string
类型与 number
类型不兼容:
let str: string = 'Hello world!';let num: number = 123;str = num; // Error: 'number' 不能赋值给 'string'num = str; // Error: 'string' 不能赋值给 'number'
TypeScript 类型系统设计比较方便,它允许你有一些不正确的行为。例如:任何类型都能被赋值给 any
,这意味着告诉编译器你可以做任何你想做的事情:
const foo: any = 123;foo = 'hello';foo.toPrecision(3);
TypeScript 对象是一种结构类型,这意味着只要结构匹配,名称也就无关紧要了:
interface Point {x: number;y: number;}class Point2D {constructor(public x: number, public y: number) {}}let p: Point;// ok, 因为是结构化的类型p = new Point2D(1, 2);
这允许你动态创建对象(就好像你在 vanilla JS 中使用一样),并且它如果能被推断,该对象仍然具有安全性。
interface Point2D {x: number;y: number;}interface Point3D {x: number;y: number;z: number;}const point2D: Point2D = { x: 0, y: 10 };const point3D: Point3D = { x: 0, y: 10, z: 20 };function iTakePoint2D(point: Point2D) {/* do something */}iTakePoint2D(point2D); // ok, 完全匹配iTakePoint2D(point3D); // 额外的信息,没关系iTakePoint2D({ x: 0 }); // Error: 没有 'y'
对类型兼容性来说,变体是一个利于理解和重要的概念。
对一个简单类型 Base
和 Child
来说,如果 Child
是 Base
的子类,Child
的实例能被赋值给 Base
类型的变量。
在由 Base
和 Child
组合的复杂类型的类型兼容性中,它取决于相同场景下的 Base
与 Child
的变体:
对于存在完全可变数据的健全的类型系统(如 JavaScript),
Invariant
是一个唯一的有效的可选变量,但是如我们说讨论的,便利性迫使我们作出一些不是很安全的选择。
当你在比较两个函数时,有一些你需要考虑的数据。
协变(Covariant):返回类型必须包含足够的数据。
interface Point2D {x: number;y: number;}interface Point3D {x: number;y: number;z: number;}let iMakePoint2D = (): Point2D => ({ x: 0, y: 0 });let iMakePoint3D = (): Point3D => ({ x: 0, y: 0, z: 0 });iMakePoint2D = iMakePoint3D;iMakePoint3D = iMakePoint2D; // ERROR: Point2D 不能赋值给 Point3D
更少的参数数量是好的(如:函数能够选择性地忽略一些多余的参数),但是你得保证有足够的参数被使用:
const foo = (x: (err: Error, data: any) => void) => {/* Do Something Else */};foo(() => null);foo(err => null);foo((err, data) => null);// Error: 参数类型 `(err: any, data: any, more: any) => null` 不能赋值给参数类型 `(err: Error, data: any) => void`foo((err, data, more) => null);
可选的(预先确定的)和 Rest 参数(任何数量的参数)都是兼容的:
let foo = (x: number, y: number) => {};let bar = (x?: number, y?: number) => {};let baz = (...args: number[]) => {};foo = bar = baz;baz = bar = foo;
可选的(上述例子中的
bar
)与不可选的(上述例子中的foo
)仅在选项为strictNullChecks
为false
时兼容
双向协变(Bivariant):旨在支持常见的事件处理方案。
// 事件等级interface Event {timestamp: number;}interface MouseEvent extends Event {x: number;y: number;}interface KeyEvent extends Event {keyCode: number;}// 简单的事件监听enum EventType {Mouse,Keyboard,}function addEventListener(eventType: EventType, handler: (n: Event) => void) {// ...}// 不安全,但是游泳池,常见。函数参数的比较是双向协变。addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));// 在安全情景下的一种不好方案addEventListener(EventType.Mouse, (e: MouseEvent) =>console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));addEventListener(EventType.Mouse, <(e: Event) => void>(((e: MouseEvent) => console.log(e.x + ',' + e.y))));// 仍然不允许明确的错误,对完全不兼容的类型会强制检查addEventListener(EventType.Mouse, (e: number) => console.log(e));
同样的,你也可以把 Array<Child>
赋值给 Array<Base>
(协变),因为函数是兼容的。数组的协变需要所有的函数 Array<Child>
都能赋值给 Array<Base>
,例如 push(t: Child)
能被赋值给 push(t: Base)
,这都可以通过函数参数双向协变实现。
下面的代码对于其他语言的开发者来说,可能会感到很困惑,因为他们认为是有错误的,可是 Typescript 并不会报错:
interface Poin2D {x: number;y: number;}let iTakePoint2D = (point: Point2D) => {};let iTakePoint3D = (point: Point3D) => {};iTakePoint3D = iTakePoint2D; // ok, 这是合理的iTakePoint2D = iTakePoint3D; // ok,为什么?
枚举与数字类型相互兼容
enum Status {Ready,Waiting,}let status = Status.Ready;let num = 0;status = num;num = status;
来自于不同枚举的枚举变量,被认为是不兼容的:
enum Status {Ready,Waiting,}enum Color {Red,Blue,Green,}let status = Status.Ready;let color = Color.Red;status = color; // Error
仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查。
class Animal {feet: number;constructor(name: string, numFeet: number) {}}class Size {feet: number;constructor(meters: number) {}}let a: Animal;let s: Size;a = s; // OKs = a; // OK
私有的和受保护的成员必须来自于相同的类。
class Animal {protected feet: number;}class Cat extends Animal {}let animal: Animal;let cat: Cat;animal = cat; // okcat = animal; // okclass Size {protected feet: number;}let size: Size;animal = size; // ERRORsize = animal; // ERROR
TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。如下例子中,T
对兼容性没有影响:
interface Empty<T> {}let x: Empty<number>;let y: Empty<string>;x = y; // ok
当 T
被成员使用时,它将在实例化泛型后影响兼容性:
interface Empty<T> {data: T;}let x: Empty<number>;let y: Empty<string>;x = y; // Error
如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:
let identity = function<T>(x: T): T {// ...};let reverse = function<U>(y: U): U {// ...};identity = reverse; // ok, 因为 `(x: any) => any` 匹配 `(y: any) => any`
类中的泛型兼容性与前文所提及一致:
class List<T> {add(val: T) {}}class Animal {name: string;}class Cat extends Animal {meow() {// ..}}const animals = new List<Animal>();animals.add(new Animal()); // okanimals.add(new Cat()); // okconst cats = new List<Cat>();cats.add(new Animal()); // Errorcats.add(new Cat()); // ok
不变性(Invariance)可能是唯一一个听起来合理的选项,这里又一个关于 contra
和 co
的变体,被认为对数组是不安全的。
class Animal {constructor(public name: string) {}}class Cat extends Animal {meow() {console.log('cat');}}let animal = new Animal('animal');let cat = new Cat('cat');// 多态// Animal <= Catanimal = cat; // okcat = animal; // Error: cat 继承于 animal// 演示每个数组形式let animalArr: Animal[] = [animal];let catArr: Cat[] = [cat];// 明显的坏处,逆变// Animal <= Cat// Animal[] >= Cat[]catArr = animalArr; // ok 如有有逆变catArr[0].meow(); // 允许,但是会在运行时报错// 另外一个坏处,协变// Animal <= Cat// Animal[] >= Cat[]animalArr = catArr; // ok 协变animalArr.push(new Animal('another animal')); // 仅仅是 push 一个 animal 至 carArr 里catArr.forEach(c => c.meow()); // 允许,但是会在运行时报错