TS第八节(类型兼容性)

237 阅读4分钟

 学习地址

TypeScript里的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。它正好与名义(nominal)类型形成对比

interface Named{
    name: string;
}
class Person{
    name: string;
}
let p: Named;
p = new Person();

在使用基于名义类型的语义,比如C#或Java中,这段代码会报错,因为Person类型没有明确说明其实现了Named接口。

TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。因为JavaScript里广泛的使用匿名对象,例如:函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。

关于可靠性的注意事项

TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是"不可靠"的。TypeScript允许这种不可靠的行为发生。

开始

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

interface Named{
    name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle'};
x = y;

这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应的属性。在这个例子中,y必须包含名字是name的string类型成员。y满足条件,因此赋值正确。

检查函数参数时使用相同的规则:

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

注意,y有个额外的location属性,但这不会引发错误。之有目标类型(这里是Named)的成员会被一一检查是否兼容。

这个比较过程是递归进行的,检查每个成员及子成员。

比较两个函数

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

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

可能会疑惑,为什么允许忽略参数,像例子y = x中那样。原因是因为忽略额外的参数在JavaScript里很常见。例如Array#forEach会给回到函数3个参数,数组元素,索引和整个数组。尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:

let items = [1, 2, 3];
items.forEach((item, index, array) => console.log(item));
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

类型系统强制原函数的返回值类型必须是目标函数返回值类型的子类。

函数参数双向协变

当比较函数参数类型时,只有当原函数参数能够赋值给目标函数或反过来赋值成功。这是不稳定的,因为调用者可能传入一个具有更精确类型的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。实际上,这极少会发生错误,并且能够实现很多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){
    /** ... **/
}
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ' , ' + e.y));

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)));

listenEvent(EventType.Mouse, (e: number) => console.log(e));

可选参数和剩余参数

比较函数兼容性的时候,可选参数与必须参数时可互换的。源类型上有额外的可选参数不报错,目标类型的可选参数在源类型里没有找到对应的参数也不报错。