• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 类型兼容性

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

    interface Pet {
      name: string;
    }
    
    class Dog {
      name: string;
    }
    
    let pet: Pet;
    // OK, because of structural typing
    pet = new Dog();
    

    在使用基于名义类型系统的语言,比如 C#或 Java 中,这段代码会报错,因为Dog类没有明确说明其实现了Pet接口。 TypeScript 的结构型类型系统是根据 JavaScript 代码的典型写法来设计的。因为 JavaScript 里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更自然。


    关于可靠性的注意事项

    TypeScript 的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript 允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。


    开始

    TypeScript 结构类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的成员。例如,思考以下代码,该代码涉及一个名为Pet的接口,接口拥有一个名为name的属性:

    interface Pet {
      name: string;
    }
    
    let pet: Pet;
    
    let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
    pet = dog;
    // dog's inferred type is { name: string; owner: string; }
    

    为了检查dog类型是否可以赋值给pet类型,编译器将检查pet的每个属性,并在dog中查看是否有对应的兼容属性。在本例中,dog必须拥有一个叫做namestring类型成员属性。dog拥有该属性,所以检查通过,赋值被允许发生。该赋值规则同样用于检查函数调用参数:

    interface Pet {
      name: string;
    }
    
    let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
    
    function greet(pet: Pet) {
      console.log("Hello, " + pet.name);
    }
    
    greet(dog); // OK
    

    注意,dog有个额外的owner属性,但这不会引发错误。只有目标类型(本例中为Pet)的成员会在兼容性检查中被考虑这个比较过程是递归进行的,将检查每个成员及子成员的类型。


    比较两个函数

    相对来讲,在比较基本类型和对象类型的时候是比较容易理解的,但如何判断两个函数是兼容的却略微复杂。下面我们从两个简单的函数入手,它们仅是参数列表略有不同:

    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, 因为 x() 缺少 location 属性
    

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


    函数参数差别

    当比较函数参数类型时,如果源参数可分配给目标参数,则分配成功,反之亦然。这是不合理的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。在实践中,这种错误是罕见的,允许这种模式是为了兼容 JavaScript 中许多常见模式。例如:

    enum EventType {
      Mouse,
      Keyboard,
    }
    
    interface Event {
      timestamp: number;
    }
    
    interface MyMouseEvent extends Event {
      x: number;
      y: number;
    }
    
    interface MyKeyEvent extends Event {
      keyCode: number;
    }
    
    function listenEvent(eventType: EventType, handler: (n: Event) => void) {
      /* ... */
    }
    
    // Unsound, but useful and common
    listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));
    
    // Undesirable alternatives in presence of soundness
    listenEvent(EventType.Mouse, (e: Event) =>
      console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
    );
    
    listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
      console.log(e.x + "," + e.y)) as (e: Event) => void);
    // Still disallowed (clear error). Type safety enforced for wholly incompatible types
    listenEvent(EventType.Mouse, (e: number) => console.log(e));
    

    你可以使用strictFunctionTypes编译选项,使 TypeScript 在这种情况下报错。注意,当strict选项为true时,默认开启该选项。


    可选参数与剩余参数

    比较函数兼容性的时候,可选参数与剩余参数是可互换的。源类型上有额外的可选参数不会产生错误,目标类型的可选参数在源类型里没有对应的参数也不会产生错误。当一个函数有剩余参数时,它被当做无限个可选参数。这对于类型系统的判断来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded

    有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:

    function invokeLater(args: any[], callback: (...args: any[]) => void) {
      /* ... 使用参数执行回调函数 ... */
    }
    
    // 不可靠 - invokeLater "可能" 提供任意数量的参数
    invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
    // 干扰:x 和 y 是切实需要的,但由于是可选参数,无法发现该错误
    invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
    


    函数重载

    对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。这确保了目标函数可以在所有源函数可调用的地方调用。


    枚举

    枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。例如:

    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; // OK, 因为y和x的结构相匹配
    

    上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

    interface NotEmpty<T> {
      data: T;
    }
    let x: NotEmpty<number>;
    let y: NotEmpty<string>;
    x = y; // Error, 因为 x 和 y 不兼容
    

    对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。然后用结果类型进行比较,就像上面第一个例子。例如:

    let identity = function <T>(x: T): T {
      // ...
    };
    
    let reverse = function <U>(y: U): U {
      // ...
    };
    
    identity = reverse; // OK, 因为 (x: any) => any 匹配 (y: any) => any
    

    目前为止,我们使用了“兼容性”,它在语言规范里没有定义。在TypeScript里,有两种兼容性:子类型和赋值。它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any来回赋值,以及enum和对应数字值之间的来回赋值。语言里的不同地方分别使用了它们之中的机制。实际上,类型兼容性是由赋值兼容性来控制的,即使在implements和extends语句也不例外。


    Any、unknown、object、void、undefined、null 和 never 的赋值性

    赋值性表格


    重申基础

    • 所有类型的值可赋值给其自身类型的变量。
    • any 和 unknown 的值在赋值给其他类型的变量时表现相同;不同在于当它作为变量类型时,不可被赋值any以外的任何类型的变量。
    • unknown 和 never 的表现接近于互相相反。所有类型的变量可被赋值unknown类型的值, never类型的变量可被赋值任意类型的值。任意类型的变量不可被赋值never类型的值, unknown类型的变量不可以被(any类型以外的)任意类型的值赋值。
    • void类型总是不可赋值或被赋值,除以下的例外情况: 1、当void类型作为变量时,仅可被赋值any、unknown类型的值;2、当void类型作为值时,仅可赋值给never、undefined和null类型的变量(当strictNullChecks被关闭,点击链接查看详情).
    • 当 strictNullChecks被关闭, null 和 undefined 的表现与 never 相似:作为变量可被赋值大部分类型的值,作为值不可赋值给大部分类型的变量,他们可以相互赋值。
    • 当 strictNullChecks被开启, null 和 undefined 的表现类似于 void:总是不可赋值或被赋值,除以下的例外情况:1、作为变量类型时,仅可被赋值any和unknown类型的值;2、作为值时,仅可赋值给Never类型的值;3、undefined类型的变量总是可被赋值void 类型的值。


    相关阅读:strictNullChecks

    默认:当strict选项开启时默认开启,其他时候默认关闭相关:strict当strictNullChecks为假时,null和undefined实际上会被语言所忽视,而这可能导致未期的错误。当strictNullChecks为真时,null和undefined将拥有自身的显著而确切的类型,此时若你在需要实际值的地方使用他们,将会产生类型错误。

    例如下列代码,users.find无法保证它一定能寻找到用户,但你可以在假设它可以找到的情况下编写代码:

    declare const loggedInUsername: string;
     
    const users = [
      { name: "Oby", age: 12 },
      { name: "Heera", age: 32 },
    ];
     
    const loggedInUser = users.find((u) => u.name === loggedInUsername);
    console.log(loggedInUser.age);
    

    设置strictNullChecks为true时,将在你无法保证loggedInUser存在的前提下,产生一个错误以阻止你的尝试使用。

    declare const loggedInUsername: string;
     
    const users = [
      { name: "Oby", age: 12 },
      { name: "Heera", age: 32 },
    ];
     
    const loggedInUser = users.find((u) => u.name === loggedInUsername);
    console.log(loggedInUser.age);
    //错误:对象loggedInUser可能为'undefined'.
    

    第二个例子的错误原因——源于Array的find方法——可以如下简化说明:

    // 当 strictNullChecks 为 true
    type Array = {
      find(predicate: (value: any, index: number) => boolean): S | undefined;
    };
    // 当 strictNullChecks 为 false,undefined 被从类型系统中移除以允许你在假设其
    // 总是返回一个结果的情况下编写代码
    type Array = {
      find(predicate: (value: any, index: number) => boolean): S;
    };
    

    上篇:类型收窄

    下篇:声明合并