TypeScript 的类型兼容性是基于结构子类型的。
结构类型是一种只使用其成员来描述类型的方式。
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
p = new Person();
// 赋值成功,因为都是结构类型,只要Person 类型的包含 Named 的属性
关于可靠性的注意事项
- TypeScript 的类型系统允许某些在编译阶段无法确认其安全性的操作。
- 例如:上面的
Named
接口 和Person
类,应该属于不同的类型,但是在 TypeScript 上只要属性包含就是兼容的。
- 例如:上面的
探究
interface Named {
name: string;
}
let x: Named;
let y = {name: 'Alice', location: 'Seattle'};
x = y; // 可以赋值
// 这里检车 `y` 是否能赋值给`x`,编译器检查`x`中的每个属性,看看是否能再`y`中找到所有对应的属性。找到了就是可以兼容,缺少属性就不能兼容
console.log(x);
function greet(n: Named) {
console.log(n.name);
}
greet(y);
// 检查属性也适用类型兼容性规则
比较两个函数
let x = (a: number) => 0;
let y = (b: number, s: number) => 0;
// y = x; // ok
// x = y; // Error
/**
* 要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类 * 型。 这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。
* 第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。
*/
为什么 x
可以赋值给 y
。就像 Array.forEach
一样
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
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
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));
可选参数和剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。
源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// 正确 - x, y 获取
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 s: number = Status.Ready;
s = 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; // OK, because y matches structure of x
x 和 y 是兼容的,因为它们的结构使用类型参数时并没有什么不同。
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible