typescript(6)- 类型兼容 | 青训营笔记

229 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第13天

「前言」

ts 针对于 js 松散的类型校验之一现状,加强了类型判断,但在很多地方,由于类型的严格,使数据传递变得极为麻烦,但是 ts 对于类型的校验不如其他强类型语言严格,对于不同类型可以存在一定的兼容性

「理解 ts 是结构类型系统」

interface Named {
  name: string;
}

class Person {
  name: string;
}

let p: Named;
p = new Person();

在这个例子中,尽管类型不同的数据还是可以相互转化的,在其他强类型语言中传递值是严格要求来源一致的,但是 ts 对于类型检查往往 不需要来源相同

因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用 结构类型系统 来描述这些类型比使用名义类型系统更好。

「类型兼容基本规则」

TypeScript 结构化类型系统的基本规则是:
如果x要兼容y,那么y至少具有与x相同的属性。

interface Named {
  name: string;
}

let x: Named;
let y = { name: '张三', age: 18 };
x = y;
y = x; // error,类型 "Named" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性。

检查函数参数时使用相同的规则:

function test(x: { name: string }) { }

let y = { name: 'zs', age: 18 }
test(y);
test({ name: 'zs', age: 18 }) // error

在使用类型兼容的时候,直接传递参数,或者在初始化与类型注解不符合的变量的时候不触发这项规则

这个规则是递归进行的,检查每个成员及子成员。

「比较不同的数据类型」

比较两个不同的函数

  1. 参数列表不同
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x;
x = y; // error,不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”。

要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。

注意:比较的是类型,不是参数名字。比较顺序,从参数列表左侧开始逐项比较,如果被比较的类型不同,则不兼容

  1. 返回值不同
let x = () => ({ name: 'Alice' });
let y = () => ({ name: 'Alice', location: 'Seattle' });

x = y;
y = x; // error, 不能将类型“() => { name: string; }”分配给类型“() => { name: string; location: string; }”。

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

  1. 可选参数与剩余参数
function invokeLater(callback: (...args: any[]) => void) { }

invokeLater((x, y) => { });
invokeLater((x, y, z) => { });
invokeLater((x?, y?) => { });

这三种将函数作为参数传递的方式都可以兼容,不论是剩余参数还是可选参数,对于超出函数内部的实际使用参数,多传几个参数相当于传入 undefined

比较枚举

enum A { red }
enum B { red }

let a = A.red;
let b = B.red;

a = b; // error
b = a; // error

当我们在初始化变量为一个枚举中的常量时,这里,这个变量的值会自动被推断为枚举类型,而不是常量的类型,相当于:

let a: A = A.red;
let b: B = B.red;

这个例子也变相的说明了:不同的枚举类型不兼容

enum A { red }
enum B { red }

let a: A = A.red;
let b: number = B.red;

a = b;
b = a;

枚举类型与数字类型相互兼容

比较类

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) { }
  show() {
    console.log(1);
  }
}

class Size {
  feet: number;
  constructor(numFeet: number) { }
  show() {
    console.log(2);
  }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK

如果两个类中的方法的参数和返回值不同,则按照比较函数的规则判断兼容性

类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

具体的说明和例子可以参考这篇文章:typescript(4)- class 详解 | 青训营笔记 - 掘金 (juejin.cn)

如果是父子类之间比较

class Father {
  skill: string;
}

class Son extends Father {
  name: string;
}

let f: Son = new Father(); // error,类型 "Father" 中缺少属性 "name",但类型 "Son" 中需要该属性。
let s: Father = new Son(); // OK

父类会兼容子类,而子类不兼容父类

比较泛型

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

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // OK

用于 ts 类型兼容是以结构性为基础的,如果被比较的内容和对应的类型相同,则相互兼容

interface Empty<T> {
  name: T;
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // error,不能将类型“string”分配给类型“number”

当内容的类型不同时,则不兼容

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。

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

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

identity = reverse;  // OK