TypeScript类型兼容性学习

518 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

今天是我学习Typescript的第5天,今天学习TypeScript类型兼容性,TypeScript里的类型兼容性是基于子类型的, 结构类型是一种只使用其成员来描述类型的方式,它与名义(nominal)类型形成对比,名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。

//定义接口
interface Animal {
    name: string;
}
//定义一个类
class Person {
    name: string;
}
let p: animal;
//typescrtipt 允许在类不继承接口的情况下 ,将类实例赋值给接口
p = new Person();

在C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Animal接口,TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。

TypeScript的类型系统在编译时允许一些类型不安全的转换。在TypeScript中如果x要兼容y,那么y至少具有与x相同的属性。

interface Animal {
    name: string;
}
let x: Animal;
//定义y
let y = { name: 'Alice', location: 'Seattle' };
//可以将y赋值给x,因为x有name属性
x = y;

这里检查y是否能赋值给x,编译器检查x中的属性,看是否能在y中也找到对应属性。 y必须包含名字是namestring类型成员,y满足条件。

function HELLO(n: Animal) {
    console.log('Hello, ' + n.name);
}
HELLO(y); // OK

y有个额外的location属性,但不会引发错误。 只有目标类型Animal会被一一递归检查是否兼容。

2.比较两个函数

在比较原始类型和对象类型的时候是容易理解的,问题是如何判断两个函数是兼容的,我们从两个简单的函数入手,它们仅是参数列表略有不同:

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并没有,所以不允许赋值。

let items = [1, 2, 3];
​
// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));
​
// Should be OK!
items.forEach((item) => console.log(item));

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

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});x = y; // OK
y = x; // Error, because x() lacks a location property

源函数的返回值类型必须是目标函数返回值类型的子类型。

3.函数参数双向协变

比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 这极少会发生错误,并且能够实现很多JavaScript里的常见模式。

enum EventType { Mouse, Keyboard }
​
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }
​
function listenEvent(eventType: EventType, handler: (n: Event) => void) {}
​
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
​
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
​
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));
4.可选参数及剩余参数

比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误,当一个函数有剩余参数时,它被当做无限个可选参数。

这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded

常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:

function invokeLater(args: any[], callback: (...args: any[]) => void) {}
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

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

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
​
let status = Status.Ready;
status = Color.Green;  

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

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

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

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
​
x = y;  // OK, because y matches structure of x

xy是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

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

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成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