• 类型兼容性
    • 关于可靠性的提醒
  • 开始
  • 比较两个函数
    • 函数参数的双边变化
  • 可选参数和rest参数
    • 有重载的函数
  • 枚举类型
    • 类中的私有成员
  • 泛型
  • 高级话题
    • 子类型 vs 赋值

    类型兼容性

    TypeScript的类型兼容性是基于结构化子类型的。它与名义类型(nominal typing)相对立。思考以下代码:

    1. interface Named {
    2. name: string;
    3. }
    4. class Person {
    5. name: string;
    6. }
    7. var p: Named;
    8. // OK, because of structural typing
    9. p = new Person();

    在大多数的名义类型编程语言中(如C#Java),以上代码将会得到一个报错,因为Person类没有明确表示它实现了Named接口。

    TypeScript的结构化类型系统是基于JavaScript的典型编码场景所设计的。由于JavaScript经常使用像函数表达式或对象字面量这样的匿名对象,利用结构化类型系统,会更适用于描述它们间的关系。

    关于可靠性的提醒

    TypeScript的类型系统允许某些不能在编译阶段就确保安全的操作。当一个类型系统允许这些时,它会被认为是不可靠的。但TypeScript之所以允许这些行为是经过严格的思考的,我们将会在下文解释原因。

    开始

    TypeScript的结构化类型系统中,最基本的原则就是,如果y至少有和x一模一样的成员,那么x就是兼容y的。例子:

    1. interface Named {
    2. name: string;
    3. }
    4. var x: Named;
    5. // y’s inferred type is { name: string; location: string; }
    6. var y = { name: 'Alice', location: 'Seattle' };
    7. x = y;

    为了检查y可否能被赋值给x,编译器会检查x中的所有属性,在y里是否有相匹配的。所以在例子里,y必须包含一个类型为stringname属性。它的确有,所以这个赋值操作是可行的。

    在检查函数调用时的参数时,规则也是一样的:

    1. function greet(n: Named) {
    2. alert('Hello, ' + n.name);
    3. }
    4. greet(y); // OK

    值得注意的是,y有一个额外的location属性,但这并不会产生一个错误。只有目标类型(例子中为Named)的成员,才会被纳入兼容性检查。

    编译器会递归地比较两个类型下的成员以及子成员。

    比较两个函数

    两个基本类型值和对象的比较是十分直观的。那么是时候来探讨两个函数的兼容性了。让我们以一组只有参数不同的函数开始:

    1. var x = (a: number) => 0;
    2. var y = (b: number, s: string) => 0;
    3. y = x; // OK
    4. x = y; // Error

    当将x赋值给y时,编译器首先会查看参数列表。y中的每一个参数都必须在x中有对应的类型兼容的参数。注意,参数名不同并没有关系,仅会考虑它们的类型。在例子里,x中的每一个参数都在y中有相兼容的参数,所以这个赋值是可行的。

    而第二个赋值操作则会报错。因为y要求有第二个参数,而x并没有。

    你或许会疑问,为什么我们在y = x的例子里,会允许“丢弃”第二个参数。这是因为,在JavaScript中,忽略后面的部分参数是很常见的做法。例如,Array#forEach提供了三个参数:数组单个元素,索引,和数组本身。但是实际使用中,人们经常只传递第一个参数:

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

    现在让我们来看看返回值,以下是两个只有返回值不同的函数:

    1. var x = () => ({name: 'Alice'});
    2. var y = () => ({name: 'Alice', location: 'Seattle'});
    3. x = y; // OK
    4. y = x; // Error because x() lacks a location property

    类型系统要求源函数的返回值是目标函数返回值的子集。

    函数参数的双边变化

    在比较两个函数参数的类型时,只要源参数是可以赋值给目标参数的,或反之,赋值都能成功。这被认为是不可靠的,因为一边的函数参数可能会被描述更精确的参数类型,但是执行是却被传入一个更宽泛的类型。在实践中,这类的错误时很罕见的,并且借此实现了很多JavaScript模式。一个简单的例子:

    1. enum EventType { Mouse, Keyboard }
    2. interface Event { timestamp: number; }
    3. interface MouseEvent extends Event { x: number; y: number }
    4. interface KeyEvent extends Event { keyCode: number }
    5. function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    6. /* ... */
    7. }
    8. // Unsound, but useful and common
    9. listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
    10. // Undesirable alternatives in presence of soundness
    11. listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
    12. listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
    13. // Still disallowed (clear error). Type safety enforced for wholly incompatible types
    14. listenEvent(EventType.Mouse, (e: number) => console.log(e));

    可选参数和rest参数

    当比较两个函数时,可选和必选参数是可以相互交换的。源函数的额外可选参数将不会导致一个报错,目标函数的不对应的可选参数也不会导致报错。

    当一个函数有rest参数时,它被视为有无限个可选参数。

    这也被认为是不可靠的,因为在大多数的运行时里,可选参数的空缺往往会被强制传入一个undefined

    下面的例子里,一个函数接受一个回调,并且使用一个(对于程序员)可预测的,但是(对于类型系统)未知数量的参数执行:

    1. function invokeLater(args: any[], callback: (...args: any[]) => void) {
    2. /* ... Invoke callback with 'args' ... */
    3. }
    4. // Unsound - invokeLater "might" provide any number of arguments
    5. invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));
    6. // Confusing (x and y are actually required) and undiscoverable
    7. invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

    有重载的函数

    当一个函数具有重载时,它重载列表中的每一个函数类型都必须与目标匹配。这保证了目标函数可以在相同的情况下被执行。

    枚举类型

    枚举类型和数字类型兼容,反之也成立。不同的枚举类型的枚举值是不兼容的。例子:

    1. enum Status { Ready, Waiting };
    2. enum Color { Red, Blue, Green };
    3. var status = Status.Ready;
    4. status = Color.Green; //error

    类的兼容性与对象字面量和接口类似,但只有一个区别:类有静态和实例部分。当比较两个类的实例时,只有实例部分会被比较。静态部分和构造函数并不会影响兼容性。

    1. class Animal {
    2. feet: number;
    3. constructor(name: string, numFeet: number) { }
    4. }
    5. class Size {
    6. feet: number;
    7. constructor(numFeet: number) { }
    8. }
    9. var a: Animal;
    10. var s: Size;
    11. a = s; //OK
    12. s = a; //OK

    类中的私有成员

    当一个类中有私有成员时,目标类必须有来自同一出处的私有成员,才会被认作是兼容的。举个例子,子类是兼容父类的,但具有相同描述的具有私有成员的两个不同类则不兼容。

    泛型

    由于TypeScript使用的是结构化类型系统,类型参数只影响其作为部分成员的结果类型。例子:

    1. interface Empty<T> {
    2. }
    3. var x: Empty<number>;
    4. var y: Empty<string>;
    5. x = y; // okay, y matches structure of x

    上述例子中,xy是兼容的,因为它们的机构体里没有以不同方式使用类型参数。让我们改变一下:

    1. interface NotEmpty<T> {
    2. data: T;
    3. }
    4. var x: NotEmpty<number>;
    5. var y: NotEmpty<string>;
    6. x = y; // error, x and y are not compatible

    这样的话,它们就有了各自独特类型的属性,就像有了非泛型属性一样。

    对于没有在内部使用过类型参数的泛型,兼容性检查会将类型参数视为any。然后再以非泛型的方式得出检测结果:

    For example,

    1. var identity = function<T>(x: T): T {
    2. // ...
    3. }
    4. var reverse = function<U>(y: U): U {
    5. // ...
    6. }
    7. identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any

    高级话题

    子类型 vs 赋值

    至今为止,我们讨论了“兼容性”,这并不是一个被定义在了语言层面的概念。在TypeScript中,有两种兼容性:子类型和赋值。它们仅有的不同是,赋值操作通过传递any的规则,拓展了子类型兼容性。

    不同的情况下,TypeScript会选择不同的兼容性。实际生产中,甚至在implementsextends语句里,类型兼容性也是由赋值兼容性所控制的。更多信息,请参阅 这里。