非常教程

Typescript参考手册

类型兼容性 | Type Compatibility

Type Compatibility

介绍

TypeScript 中的类型兼容性基于结构化子类型。结构打字是仅基于成员关联类型的一种方式。这与名义打字相反。考虑下面的代码:

interface Named {
  name: string;
}

class Person {
  name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

在 C# 或 Java 等名义类型的语言中,等效的代码将是一个错误,因为Person类没有明确地将自己描述为Named接口的实现者。

TypeScript 的结构类型系统是基于通常编写 JavaScript 代码的方式设计的。由于 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是一个字符串。它的确如此,因此分配是允许的。

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

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

请注意,它y有一个额外的location属性,但这不会产生错误。Named在检查兼容性时,只考虑目标类型的成员(在这种情况下)。

该比较过程递归地进行,探索每个成员和子成员的类型。

比较两个功能

While comparing primitive types and object types is relatively straightforward, the question of what kinds of functions should be considered compatible is a bit more involved. Let’s start with a basic example of two functions that differ only in their parameter lists:

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为回调函数提供三个参数:数组元素,其索引和包含数组。不过,提供仅使用第一个参数的回调会非常有用:

let items = [1, 2, 3];

// Don't force these extra parameters
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

类型系统强制源函数的返回类型是目标类型返回类型的子类型。

函数参数双变量

在比较函数参数的类型时,如果源参数可分配给目标参数,则分配成功,反之亦然。这是不合适的,因为调用者最终可能会得到一个采用更专门化类型的函数,但会调用具有较少专用类型的函数。实际上,这种错误很少见,并且允许使用许多常见的 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));

可选参数和其余参数

在比较兼容性功能时,可选和必需的参数是可以互换的。源类型的额外可选参数不是错误,并且源类型中没有相应参数的目标类型的可选参数不是错误。

当一个函数有一个 rest 参数时,它被当作是无限的一系列可选参数。

从类型系统的角度来看,这是不合理的,但是从运行时的角度来看,可选参数的思想一般不会很好实施,因为通过undefined这个位置对于大多数函数来说是等价的。

这个动机的例子是一个函数的通用模式,该函数接受一个回调,并用一些可预测的(对程序员)但是对类型系统不知道的参数数量调用它:

function invokeLater(args: any[], callback: (...args: any[]) => void) {
  /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));

带过载的功能

当函数有重载时,源类型中的每个重载必须与目标类型上的兼容签名匹配。这确保了目标函数可以在所有与源函数相同的情况下被调用。

Enums

枚举与数字兼容,并且数字与枚举兼容。来自不同枚举类型的枚举值被认为是不兼容的。例如,

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

私人和受保护的成员在课堂上

班级中的私人和受保护的成员会影响其兼容性。当检查类的实例的兼容性时,如果目标类型包含私有成员,则源类型还必须包含源自相同类的私有成员。同样,对于具有受保护成员的实例也是如此。这允许一个类与其超类的赋值兼容,但不能与其他具有相同形状的继承层次结构的类相匹配。

泛型

因为 TypeScript 是一个结构类型系统,所以类型参数仅在作为成员类型的一部分使用时影响结果类型。例如,

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

x = y;  // okay, y matches structure of x

在上面,x并且y是兼容的,因为它们的结构不以区分方式使用类型参数。通过添加一个成员来更改这个例子来Empty<T>说明这是如何工作的:

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

x = y;  // error, x and y are not compatible

通过这种方式,具有指定类型参数的泛型类型就像非泛型类型一样。

对于没有指定类型参数的泛型类型,通过指定any所有未指定的类型参数来检查兼容性。然后检查结果类型的兼容性,就像在非泛型情况下一样。

例如,

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

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

identity = reverse;  // Okay because (x: any)=>any matches (y: any)=>any

高级主题

子类型与分配

到目前为止,我们已经使用'兼容',这不是在语言规范中定义的术语。在 TypeScript 中,有两种兼容性:子类型和赋值。这些不同之处仅在于赋值扩展了子类型与规则的兼容性,以允许将any枚举与相应的数值进行分配,以及从枚举到和从枚举分配。

语言中的不同位置根据情况使用两种兼容性机制之一。出于实用目的,即使在implementsextends子句的情况下,类型兼容性也由赋值兼容性决定。有关更多信息,请参阅 TypeScript 规范

类型兼容性 | Type Compatibility相关

Typescript

TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。

主页 https://www.typescriptlang.org
源码 https://github.com/Microsoft/TypeScript
发布版本 2.6.0