TypeScript学习笔记(七)类型兼容性

324 阅读4分钟

类型兼容性

TypeScript 的类型兼容性是基于结构类型的,与之相对的是名义类型,结构类型是基于成员类型来描述类型的方式,就是数据类型是由成员类型来决定的,而名义类型中,数据类型的兼容性和等价性都是由明确的声明和类型的名称来决定的。 例子:

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// 正确,因为是基于结构类型的类型兼容性。
p = new Person();

在基于名义类型的类型兼容性中,上面的代码会报错,因为 Person 并没有声明实现了 Named 接口。但基于结构类型的类型兼容性中,Person 类的结构是实现了 Named 接口的,所以它符合结构类型的兼容性标准。

关于可靠性的注意事项

TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。

开始

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。

interface Named {
    name: string;
}

// y 的推断类型是:{ name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };

let x: Named;
x = y; // 赋值正确

function greet(n: Named) {
    alert('Hello, ' + n.name);
}
greet(y); // OK

y 符合必须包含字符串类型的name属性的接口要求,所以赋值正确,检查函数参数时也是一样的规则。

比较两个函数

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

y = x; // OK
x = y; // Error

要查看 x 能否赋值给 y,首先要查看他们的参数列表, x 的参数列表中的每个参数都能在 y 中找到对应的类型,参数名无所谓只要类型一致即可。 但是 x 中没有 y 函数的参数列表中的 string 类型的参数,所以赋值失败。

如何处理返回值类型

下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error because x() lacks a location property

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。 源函数就是被赋值的函数,目标函数就是赋值给别的函数的那个函数

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。

可选参数及剩余参数

在比较函数兼容性的审核,源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。

函数重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。

枚举

枚举类型与数字类型相互兼容,但是不同枚举类型之间不兼容。

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  //error

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  //OK
s = a;  //OK

类的私有成员

私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // 正确, y 与 x 的结构匹配

上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // 错误, x 与 y 不兼容

此时 x 与 y的结构是不同的。

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较:

let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // 正确: 因为 (x: any)=>any 匹配 (y: any)=>any