TypeScript 自学笔记8 类型兼容性

介绍

  • TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface Named {
    name: string;
    }

    class Person {
    name: string;
    }

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

开始

属性赋值

  • TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性
  • 这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。 在这个例子中,y必须包含名字是name的string类型成员。y满足条件,因此赋值正确
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 接口
    interface Named {
    name: string;
    }

    let x: Named;
    // y's inferred type is { name: string; location: string; }
    let y = { name: 'Alice', location: 'Seattle' };
    x = y;

    console.log(x)
    // { name: 'Alice', location: 'Seattle' }

  • 检查函数参数
  • y有个额外的location属性,但这不会引发错误。 只有目标类型(这里是Named)的成员会被一一检查是否兼容
  • 只有相同给规则才会被提取出来
  • 这个比较过程是递归进行的,检查每个成员及子成员
    1
    2
    3
    4
    5
    function greet(n: Named) {
    console.log('Hello, ' + n.name);
    console.log('test', + n.location)// error
    }
    greet(y);

比较两个函数

  • 相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手
  • 要赋值首先要判断参数个数和参数类型,查看赋值中的参数是否被赋值中都有对于都参数存在

    1
    2
    3
    4
    5
    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个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的
    1
    2
    3
    4
    5
    6
    7
    8
    let items = [1, 2, 3];

    // Don't force these extra arguments
    items.forEach((item, index, array) => console.log(item));

    // Should be OK!
    items.forEach((item) => console.log(item));
    // 123123

  • 下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数
  • 依然结果还是一样的

    1
    2
    3
    4
    let x = () => ({name: 'Alice'});
    let y = () => ({name: 'Alice', location: 'Seattle'});
    x = y; // OK
    y = x; // Error, because x() lacks a location property
  • 类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型

函数参数双向协变

  • 当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式
  • 下面的例子都是指定着一些不安全的写法不允许的写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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));

可选参数及剩余参数

  • 比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误
  • 当一个函数有剩余参数时,它被当做无限个可选参数
  • 这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded
  • 其实用可选和不用是一样的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* ... Invoke callback with 'args' ... */
    // 调用1
    callback(args)
    // 1,2, undefined
    // 1,2, undefined
    // 调用2
    callback(...args)
    // 1,2
    // 1,2
    // 调用3
    callback()
    // undefined,undefined
    // undefined,undefined
    }

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

枚举

  • 枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的
    1
    2
    3
    4
    5
    6
    7
    8
    enum Status { Ready, Waiting };
    enum Color { Red, Blue, Green };

    // 这里如果使用status的命名接收也是会报错的
    let test = Status.Ready; //0
    test = Color.Green; // Error ,虽然报错但是执行js文件是可以输出2的

    console.log(test)

  • 类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
    }

    class Size {
    feet: number;
    // 1.如果加入一个string在这里
    test: string
    constructor(numFeet: number) { }
    }

    let a: Animal;
    let s: Size;

    a = s; // OK
    s = a; // OK , 2.这里就无法赋值了

类的私有成员和受保护成员

  • 的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
    }

    class Size {
    private feet: number
    constructor(numFeet: number) { }
    }

    let a: Animal;
    let s: Size;

    a = s; // error
    s = a; // error

泛型

  • 因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型

    1
    2
    3
    4
    5
    6
    interface Empty<T> {
    }
    let x: Empty<number>;
    let y: Empty<string>;

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

    1
    2
    3
    4
    5
    6
    7
    8
    interface Empty<T> {
    data: T
    }
    let x: Empty<number>;
    let y: Empty<string>;

    x = y; //error
    // 因为用到了具体实例上,就不再是范型就存在差异了

  • 对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let identity = function<T>(x: T): T {
    // ...
    }

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

    identity = reverse;

后记

  • 这个就是我学习Ts的第八天的笔记,后面学习ts的高级类型,欢迎更多的同行大哥指导交流
  • 欢迎进入我的博客https://yhf7.github.io/
  • 如果有什么侵权的话,请及时添加小编微信以及qq也可以来告诉小编(905477376微信qq通用),谢谢!
-------------本文结束感谢您的阅读-------------
0%