一文带你了解TypeScript类型兼容性

叁歲伎倆 2022-12-31 11:28 258阅读 0赞

目录

      • 本文概览:
        1. 什么是类型兼容性?
        1. 函数类型兼容性
        • (1)函数参数个数
        • (2)函数参数类型
        • (3)剩余参数和可选参数
        • (4)函数参数双向协变
        • (5)函数返回值类型
        • (6)函数重载
        1. 枚举类型兼容性
        1. 类类型兼容性
        1. 泛型类型兼容性

本文概览:

在这里插入图片描述

1. 什么是类型兼容性?

JavaScript是一门弱类型语言,它对类型是弱校验,正因为这个特点,多以才有了TypeScript这个强类型语言系统的出现,来弥补类型检查的短板。TypeScript在实现类型强校验的同时,还要满足JavaScript灵活的特点,所以就有了类型兼容性这个概念。

所谓的类型兼容性用于确定一个类型是否能赋值给其他类型。这看起来用处不大,实际上了解类型兼容性可以避免在实际的编程中一些低级错误的出现。

TypeScript中的类型兼容性是基于结构类型的,结构类型是一种只使用其成员来描述类型的方式。其基本原则是,如果x要兼容y,那么y至少要具有与x相同的属性。

下面来看一个例子,我们构建一个类Teacher,然后声明一个接口Student,Student的属性Teacher都有,而且还多了其他的属性,这种情况下Student兼容了Teacher:

  1. class Teacher{
  2. constructor(public weight: number, public name: string, public job: string) {
  3. }
  4. }
  5. interface Student {
  6. name: string
  7. weight: number
  8. }
  9. let x: Student
  10. x = new Teacher(120, 'TS', 'teacher') // OK

如果反过来,Teacher并没有兼容Student,因为Student的属性比Person少了一个。

2. 函数类型兼容性

函数的类型兼容性主要包括以下六个方面:

(1)函数参数个数

函数参数个数如果要兼容,需要满足一个要求:如果对函数 y 进行赋值,那么要求 x 中的每个参数都应在 y 中有对应,也就是 x 的参数个数小于等于 y 的参数个数

  1. let x = (a: number) => 0;
  2. let y = (b: number, c: string) => 0;

上面定义的两个函数,如果进行赋值的话,来看下两种情况的结果:

  1. y = x; // 没问题

将 x 赋值给 y 是可以的,因为如果对函数 y 进行赋值,那么要求 x 中的每个参数都应在 y 中有对应,也就是 x 的参数个数小于等于 y 的参数个数,而至于参数名是否相同是无所谓的。

  1. x = y; // error Type '(b: number, s: string) => number' is not assignable to type '(a: number) => number'

这个例子中,y 要赋值给 x,但是 y 的参数个数要大于 x,所以报错。

再来看一个例子:

  1. const arr = [1, 2, 3];
  2. arr.forEach((item, index, array) => {
  3. console.log(item);
  4. });
  5. arr.forEach(item => {
  6. console.log(item);
  7. });

这个例子中,传给 forEach 的回调函数的参数是三个,但是可以只用一个,这样就只需写一个参数。我们传入的 forEach 的函数是 forEach 的参数,它是一个函数,这个函数的参数列表是定义在 forEach 方法内的,我们可以传入一个参数少于等于参数列表的函数,但是不能传入一个比参数列表参数个数还多的函数。后面的遍历形式就对前面的遍历形式进行了兼容。

(2)函数参数类型

除了参数个数,参数的类型也需要对应:

  1. let x = (a: number) => 0;
  2. let y = (b: string) => 0;
  3. x = y; // error 不能将类型“(b: string) => number”分配给类型“(a: number) => number”。

可以看到 x 和 y 两个函数的参数个数和返回值都相同,只是参数类型对不上,所以也是不行的。

(3)剩余参数和可选参数

当要被赋值的函数参数中包含剩余参数(…args)时,赋值的函数可以用任意个数参数代替,但是类型需要对应:

  1. const getNum = ( // 这里定义一个getNum函数,他有两个参数
  2. arr: number[], // 第一个参数是一个数组
  3. callback: (...args: number[]) => number // 第二个参数是一个函数,这个函数的类型要求可以传入任意多个参数,但是类型必须是数值类型,返回值必须是数值类型
  4. ): number => {
  5. return callback(...arr); // 这个getNum函数直接返回调用传入的第二个参数这个函数,以第一个参数这个数组作为参数的函数返回值
  6. };
  7. getNum(
  8. [1, 2],
  9. (...args: number[]): number => args.length // 这里传入一个函数,逻辑是返回参数的个数
  10. );

剩余参数其实可以看做无数个可选参数,所以在兼容性方面是差不多的,再俩看个可选参数和剩余参数结合的例子:

  1. const getNum = (
  2. arr: number[],
  3. callback: (arg1: number, arg2?: number) => number // 这里指定第二个参数callback是一个函数,函数的第二个参数为可选参数
  4. ): number => {
  5. return callback(...arr); // error 应有 1-2 个参数,但获得的数量大于等于 0
  6. };

这里因为arr可能为空数组或不为空,如果为空数组则…arr不会给callback传入任何实际参数,所以这里报错。如果我们换成return callback(arr[0], …arr)就没问题了。

(4)函数参数双向协变

函数参数双向协变即参数类型无需绝对相同

  1. let funcA = function(arg: number | string): void { };
  2. let funcB = function(arg: number): void { };
  3. // funcA = funcB 和 funcB = funcA都可以

在这个例子中,funcA 和 funcB 的参数类型并不完全一样,funcA 的参数类型为一个联合类型 number | string,而 funcB 的参数类型为 number | string 中的 number,他们两个函数也是兼容的。

(5)函数返回值类型

  1. let x = (a: number): string | number => 0;
  2. let y = (b: number) => "a";
  3. let z = (c: number) => false;
  4. x = y; // ok
  5. x = z; // 不能将类型“(c: number) => boolean”分配给类型“(a: number) => string | number”

这个例子中 x 函数的返回值是联合类型,也就是返回值既可以是 string 类型也可以是 number 类型。而 y 的返回值类型是 number 类型,参数个数和类型也没问题,所以可以赋值给 x。而 z 的返回值类型 false 并不是 string 也不是 number,所以不能赋值。

(6)函数重载

带有重载的函数,要求被赋值的函数的每个重载都能在用来赋值的函数上找到对应的签名:

  1. function merge(arg1: number, arg2: number): number; // 这是merge函数重载的一部分
  2. function merge(arg1: string, arg2: string): string; // 这也是merge函数重载的一部分
  3. function merge(arg1: any, arg2: any) { // 这是merge函数实体
  4. return arg1 + arg2;
  5. }
  6. function sum(arg1: number, arg2: number): number; // 这是sum函数重载的一部分
  7. function sum(arg1: any, arg2: any): any { // 这是sum函数实体
  8. return arg1 + arg2;
  9. }
  10. let func = merge;
  11. func = sum; // error 不能将类型“(arg1: number, arg2: number) => number”分配给类型“{ (arg1: number, arg2: number): number; (arg1: string, arg2: string): string; }”

sum函数的重载缺少参数都为string返回值为string的情况,与merge函数不兼容,所以赋值时会报错。

3. 枚举类型兼容性

数字枚举成员类型与数字类型互相兼容:

  1. enum Status {
  2. On,
  3. Off
  4. }
  5. let s = Status.On;
  6. s = 1;
  7. s = 3;

虽然Status.On的值是0,但是这里数字枚举成员类型和数值类型互相兼容,所以这里给s赋值为3也没问题。但是不同枚举值之间是不兼容的:

  1. enum Status {
  2. On,
  3. Off
  4. }
  5. enum Color {
  6. White,
  7. Black
  8. }
  9. let s = Status.On;
  10. s = Color.White; // error Type 'Color.White' is not assignable to type 'Status'

虽然 Status.On 和 Color.White 的值都是 0,但它们是不兼容的。

字符串枚举成员类型和字符串类型是不兼容的:

  1. enum Status {
  2. On = 'on',
  3. Off = 'off'
  4. }
  5. let s = Status.On
  6. s = 'TypeScript' // error 不能将类型"TypeScript"分配给类型“Status”

这里会报错,因为字符串字面量类型'TypeScript'和Status.On是不兼容的。

4. 类类型兼容性

比较两个类类型的值的兼容性时,仅仅只有实例成员和方法会相比较,类的静态成员和构造函数不进行比较

  1. class Animal {
  2. static age: number;
  3. constructor(public name: string) { }
  4. }
  5. class People {
  6. static age: string;
  7. constructor(public name: string) { }
  8. }
  9. class Food {
  10. constructor(public name: number) { }
  11. }
  12. let a: Animal;
  13. let p: People;
  14. let f: Food;
  15. a = p; // ok
  16. a = f; // error Type 'Food' is not assignable to type 'Animal'

Animal类和People类都有一个age静态属性,它们都定义了实例属性name,且name的类型都是string。把类型为People的p赋值给类型为Animal的a没有问题,因为类类型比较兼容性时,只比较实例的成员,这两个变量虽然类型是不同的类类型,但是它们都有相同字段和类型的实例属性name,而类的静态成员是不影响兼容性的,所以它俩兼容。而类Food定义了一个实例属性name,类型为number,所以类型为Food的f与类型为Animal的a类型不兼容,不能赋值。

类的私有成员和受保护成员:
类的私有成员和受保护成员会影响兼容性。当检查类的实例兼容性时,如果目标(也就是要被赋值的那个值)类型(这里实例类型就是创建它的类)包含一个私有成员,那么源(也就是用来赋值的值)类型必须包含来自同一个类的这个私有成员,这就允许子类赋值给父类:

  1. class Parent {
  2. private age: number;
  3. constructor() { }
  4. }
  5. class Children extends Parent {
  6. constructor() {
  7. super();
  8. }
  9. }
  10. class Other {
  11. private age: number;
  12. constructor() { }
  13. }
  14. const children: Parent = new Children();
  15. const other: Parent = new Other(); // 不能将类型“Other”分配给类型“Parent”。类型具有私有属性“age”的单独声明

当指定 other 为 Parent 类类型,给 other 赋值 Other 创建的实例的时候,会报错。因为 Parent 的 age 属性是私有成员,外界是无法访问到的,所以会类型不兼容。而children的类型我们指定为了Parent类类型,然后给它赋值为Children类的实例,没有问题,是因为Children类继承Parent类,且实例属性没有差异,Parent类有私有属性age,但是因为Children类继承了Parent类,所以可以赋值。

同样,使用 protected 受保护修饰符修饰的属性,也是一样的。

  1. class Parent {
  2. protected age: number;
  3. constructor() { }
  4. }
  5. class Children extends Parent {
  6. constructor() {
  7. super();
  8. }
  9. }
  10. class Other {
  11. protected age: number;
  12. constructor() { }
  13. }
  14. const children: Parent = new Children();
  15. const other: Parent = new Other(); // 不能将类型“Other”分配给类型“Parent”。属性“age”受保护,但类型“Other”并不是从“Parent”派生的类

5. 泛型类型兼容性

泛型包含类型参数,这个类型参数可能是任意类型,使用时类型参数会被指定为特定的类型,而这个类型只影响使用了类型参数的部分:

  1. interface Data<T> { }
  2. let data1: Data<number>;
  3. let data2: Data<string>;
  4. data1 = data2; // ok

data1 和 data2 都是 Data 接口的实现,但是指定的泛型参数的类型不同,TS 是结构性类型系统,所以上面将 data2 赋值给 data1 是兼容的,因为 data2 指定了类型参数为 string 类型,但是接口里没有用到参数 T,所以传入 string 类型还是传入 number 类型并没有影响。

再来看个例子:

  1. interface Data<T> {
  2. data: T;
  3. }
  4. let data1: Data<number>;
  5. let data2: Data<string>;
  6. data1 = data2; // error 不能将类型“Data<string>”分配给类型“Data<number>”。不能将类型“string”分配给类型“number”

现在结果就不一样了,赋值时报错,因为 data1 和 data2 传入的泛型参数类型不同,生成的结果结构是不兼容的。

发表评论

表情:
评论列表 (有 0 条评论,258人围观)

还没有评论,来说两句吧...

相关阅读