ts-learnings/09_type_compatibility.md
2019-04-03 17:53:14 +08:00

15 KiB
Raw Blame History

类型兼容性

Type Compatibility

简介

TypeScript中的类型兼容性是基于结构化子类型赋予的。结构化的类型赋予是一种仅依靠类型的成员而将这些类型联系起来的方式。这一点与名义上的类型赋予有所不同Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing。请考虑以下代码

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;

// 没有问题,因为这里的结构化类型赋予
p = new Person();

在诸如C#或Java这样的 名义类型语言 中,等效代码将报出错误,因为类Person并未显式地将其描述为是Named接口的一个 实现器 In nominally-typed languages like C# or Java, the equivalent code would be an error because the Person class does not explicity describe itself as being an an implementor of the Named interface

TypeScript的结构化类型系统是基于JavaScript代码的一般编写方式而设计的。因为JavaScript广泛用到诸如函数表达式及对象字面值这样的匿名对象因此使用结构化类型系统而非名义类型系统对于表示JavaScript的那些库中所发现的关系种类就更加自然一些TypeScript's structural type system was designed based on how JavaScript code is typically written. Because JavaScript widely uses anonymous objects like function expressions and object literals, it's much more natural to represent the kinds of relationships found in JavaScript libraries with a structural type system instead of a nominal one

关于可靠性/健全性的说明A Note on Soundness

TypeScript的类型系统令到一些在编译时无法知晓的操作是安全的。当某个类型系统具备了此种属性时就说其不是“健全的”。至于TypeScript允许在哪里存在不健全行为则是被仔细考虑过的贯穿本文这里将对这些特性于何处发生以及它们背后的动机场景加以解释TypeScript's type system allows certain operations that can't be known at compile-time to be safe. When a type system has this property, it is said to not be "sound". The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we'll explain where these happen and the motivating scenarios behind them

开始Starting out

TypeScript的结构化类型系统的基本规则就是在y具备与x相同成员时,x就兼容y。比如:

interface Named {
    name: string;
}

let x: Named;

// y 所引用的类型是 { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };

x = y;

编译器要检查这里的y是否可以被赋值给x,就会对x的各个属性进行检查,以在y中找到相应的兼容属性。在本例中,y必须具有一个名为name的字符串成员。而它确实有这样的一个成员,因此该赋值是允许的。

interface Named {
    name: string;
    age: number;
}

let x: Named;

// y 所引用的类型是 { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };

// TSError:  Unable to compile TypeScript
// src/main.ts (12,1): Type '{ name: string; location: string; }' is not assignable to type 'Named'.
//  Property 'age' is missing in type '{ name: string; location: string; }'. (2322)
x = y;

在对函数调用参数进行检查时也使用到通用的赋值规则The same rule for assignment is used when checking function call arguments

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

greet(y); // 没有问题

注意这里的y有着一个额外的location属性,但这并不会造成错误。在对兼容性进行检查时,仅会考虑目标类型(这里也就是Named)的那些成员。

该比较过程是递归进行的对每个成员及子成员进行遍历This comparison process proceeds recursively, exploring the type of each member and sub-member

两个函数的比较Comparing two functions

可以看出对原生类型与对象类型的比较是相对直接的而何种函数应被看着是兼容的这个问题就牵扯到更多方面了While comparing primitive types and object types is relatively straightforward, the question of what kinds of functions should be considered is a bit more involved。下面就以两个仅在参数清单上不同的函数的基本示例开始

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

y = x; // 没有问题

// TSError:  Unable to compile TypeScript
// src/main.ts (9,1): Type '(b: number, s: string) => number' is not assignable to type '(a: number) => number'. (2322)
x = y; // 错误

为检查x是否可被赋值给y,首先要看看参数清单。x中的每个参数,在y中都必须有一个类型兼容的参数与其对应。注意参数名称是不考虑的,考虑的仅是它们的类型。在本示例中,函数x的每个参数,在y中都有一个兼容的参数与其对应,因此该赋值是允许的。

第二个赋值是错误的赋值,因为y有着必要的第二个参数,x并没有,因此该赋值是不允许的。

对于示例中y = x之所以允许“丢弃”参数的原因在JavaScript中此种忽略额外函数参数的赋值实际上是相当常见的。比如Array#forEach方法就提供了3个参数给回调函数数组元素、数组元素的索引以及所位处的数组。不过给其一个仅使用首个参数的回调函数仍然是很有用的

let items = [1, 2, 3];

// Don't force these extra parameters
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; // 没有问题

// TSError:  Unable to compile TypeScript
// src/main.ts (6,1): Type '() => { name: string; }' is not assignable to type '() => { name: string; location: string; }'.
// Type '{ name: string; }' is not assignable to type '{ name: string; location: string; }'.
// Property 'location' is missing in type '{ name: string; }'. (2322)
y = x; // 错误,因为`x`缺少一个location属性

类型系统强制要求 源函数 的返回值类型,是 目标函数 返回值类型的一个子集The type system enforces that the source function's return type be a subtype of the target type's return type

函数参数的双向协变Funtion Parameter Bi-variance

在对函数参数的类型进行比较时加入源参数可被赋值给目标参数或目标参数可赋值给源参数那么函数间的赋值将成功。这是不完备的因为某个调用器可能以被给予一个取更为具体类型的函数却以不那么具体类型来触发该函数而结束。在实践中此类错误很少见同时此特性带来了很多常见的JavaScript模式When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the funtion with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns。下面是一个简要的示例

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

// 下面这样写是仍然不允许的肯定是错的。因为完全不兼容类型而强制开启类型安全Still disallowed (clear erro). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

可选参数与其余参数Optional Parameters and Rest Parameters

在出于兼容性而对函数加以比较时可选参数与必需参数是通用的。源类型的额外可选参数并不是一个错误同时目标类型的、在源类型中没有对应参数的可选参数也不是一个错误When comparing functions for compatibility, optional and required parameters are interchangeable. Extra optional parameters of the source type are not an error, and optional parameters of the target type without corresponding parameters in the source type are not an error

在某个函数具有其余参数时其余参数就被当成是有无限个可选参数加以对待When a function has a rest parameter, it is treated as if it were an infinite series of optional parameters

这一点从类型系统角度看是不完备的,但因为对于大多数函数来数,在那个位置传递undefined都是等效的因此从运行时角度可选参数这一概念通常并不是精心构思的This is unsound from a type system perspective, but from a runtime point of view the idea of an optional parameter is generally not well-enforced since passing undefined in that position is equivalent for most functions

下面的示例就是某个取一个回调函数并以可预测对于程序员却未知对于类型系统数量的参数来触发该回调函数的函数的常见模式The motivating example is the common pattern of a function that takes a callback and invokes it with some predictable(to the programmer) but unknown(to the type system) number of arguments:

function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* 以 args 来触发回调函数 */
}

// 不完备 -- invokeLater "可能" 提供任意数量的参数
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

// 混乱x与y实际上是必需的且无法发现Confusing ( x and y are actually required ) and undiscoverable 
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));

带过载的函数Functions with overloads

当函数有着过载时那么源类型中的每个过载在目标类型上都必须有一个兼容的签名与其匹配。这样才能确保目标函数可与源函数所在的同样场合进行调用When a function has overloads, each overload in the source type must be matched by a compatible signature on the target type. This ensures that the target function can be called in all the same situation as the source function

枚举的兼容性

枚举与数字兼容,同时数字也与枚举兼容。不同枚举类型的枚举值,被看着是兼容的。比如:

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

let status = Status.Ready;
status = Color.Green; // 没毛病

类的兼容性Classes

类的兼容性与对象字面值及接口类似但有一个例外类同时有着静态与实例类型Classes have both a static and an instance type。在对某个类类型的两个对象进行比较时仅比较实例的成员。静态成员与构造器并不影响兼容性。

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

类中的私有与受保护成员

类中的私有与受保护成员,对类的兼容性有影响。在对类的某个实例进行兼容性检查时,如目标类型包含了一个私有成员,那么源类型也必须要有一个从同样类继承的私有成员。与此类似,同样的规则也适用与有着受保护成员的实例。这就令到类可被兼容的赋值给其超类,但却 不能 兼容的赋值给那些来自不同继承层次、除此之外有着同样外形的类This allows a class to be assignment compatible with its super class, but not with classes from a different inheritance hierarchy which otherwise have the same shape

泛型Generics

因为TypeScript是一个结构化的类型系统a structural type system所以类型参数在作为某成员类型一部分而被消费是其仅影响最终类型。比如

interface Empty<T> {}

let x: Empty<number>;
let y: Empty<string>;

x = y; //没有问题y 与 x 的解构匹配

在上面的代码中,xy是兼容的,因为它们的解构没有以各异的方式来使用那个类型参数。如通过加入一个成员到Empty<T>中,而对此示例进行修改,就可以反映出这一点:

interface NotEmpty<T> {
    data: T;
}

let x: NotEmpty<number>;
let y: NotEmpty<string>;

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

这种情况下有着上面这种指定类型参数的泛型与一个非通用类型的表现一致In this way, a generic type that has its type arguments specified acts just like a non-generic type

对于没有指定类型参数的泛型,兼容性的检查,是通过在所有未指定类型参数的地方指定any进行的。随后对最终类型进行兼容性检查就跟非通用类型一样For generic types that do not have their type arguments specified, compatibility is checked by specifying any in place of all unspecified type arguments. Then resulting types are then checked for compatibility, just as in the non-generic case

比如,

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

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

identity = reverse; //没有问题,因为(x: any)=>any 与(y: any)=>any是匹配的

高级话题Advanced Topics

子类型与赋值语句Subtype vs Assignment

到目前为止都使用的是“兼容性”一词但这个说法在语言规格中并没有对其进行定义。在TypeScript中兼容有两种子类型与赋值。它们的不同仅在于赋值以允许赋值给与从any以及赋值给及从有着对应的数字值的枚举这两个规则对子类型进行了拓展In TypeScript, there are two kinds of compatibility: subtype and assignment. These differ only in that assignment extends subtype compatibility with rules to allow assignment to and from any and to and from enum with corresponding numeric values

根据不同情况,语言中的不同地方会使用这两种兼容性机制之一。实际来看,就算有着implementsextends关键字类型兼容性仍按赋值兼容性看待Different places in the language use one of the two compatibility mechanisms, depending on the situation. For practical purposes, type compatibility is dicated by assignment compatibility even in the cases of the implements and extends clauses。更多信息请查阅TypeScript规格。