阅读 47

构建 Typescript 知识体系(六)-类型检查机制

这是我参与更文挑战的第十二天,活动详情查看:更文挑战

类型检查机制是什么

typescript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。

类型检查机制有什么作用

辅助开发,提高开发效率

类型检查机制分类

  • 类型推断
  • 类型兼容性
  • 类型保护

类型推断

不需要指定变量的类型或者函数的返回值类型,typescript 可以根据某些规则自动地为其推断出一个类型

  • 基础类型推断
  • 最佳通用类型推断
  • 上下文类型推断

基础类型的类型推断

使用场景:初始化一个变量的时候

// TS推断结果:  let a: number
let a = 1;

// TS推断结果:   let b: any[]
let b = [];

// let c: number[]
let c = [1];

// let d: string[]
let d = ["a"];
复制代码

也适用于 设置函数默认参数 的时候

// (parameter) x: number
let a1 = (x = 1) => {
  return x;
};

// let c: (x?: number) => number
let c = (x = 1) => {
  return x + 1;
};
复制代码

也适用于 确定 函数返回值 的时候

// let a2: (x?: number) => number
let a2 = (x = 1) => {
  const res = x + 2;
  return res;
};
复制代码

最佳通用类型推断

当需要从多个类型中推断出一个类型的时候,TS 就会尽可能的推断出一个兼容当前所有类型的通用类型

// let b1: (number | null)[]
let b1 = [1, null];
复制代码

1 和 null 是不兼容的两种类型,TS 就会推断出以 number 和 null 组合而成的 联合类型

注意: 在 tsconfig.json 中修改如下配置后,结果类型检查结果又会改变

...
"strictNullChecks": false,
...
复制代码
// let b1: number[]
let b1 = [1, null];
复制代码

总结

以上的类型推断,都是从右向左推断方式, 即根据表达式右侧的值推断 表达式左侧变量的类型 还有一种,从左到右的推断方式,即上下文类型推断,通常发生一个事件处理中

上下文类型推断

通常发生在事件处理中,

// ts或根据左侧时间绑定,来推断右侧事件的类型
window.onkeydown = (event: KeyboardEvent) => {
  console.log(event.AT_TARGET);
};

/*
最新版本的vscode中,输入window.onkeydown = (event) => { console.log('onkeydown')},
编辑器会提示参数“event”隐式具有“any”类型,而没有进行上下文推断。
解决:加上 KeyboardEvent 类型就可以了
*/
复制代码

有时候 TS 推断不符合预期,而且开发人员比 TS 更加了解代码的时候,TS 允许开发人员手动覆盖 TS 推断(类型断言) **

类型断言

let foo = {};
//  foo这个空对象上没有bar属性, 如何解决呢
foo.bar = 1;
复制代码
interface Foo {
  bar: number;
}

let foo = {} as Foo;

foo.bar = 1;
复制代码

类型断言可以增加代码的灵活性,在改造一些旧代码时非常有效, 但类型断言不能滥用,需要对上下文环境有充足的预判, 建议在申明的时候就指定所有的类型

interface Foo {
  bar: number;
}
// 滥用类型断言后 没有报错
let foo = {} as Foo;
复制代码

在申明的时候就指定所有的类型

interface Foo {
  bar: number;
}

/*
类型 "{}" 中缺少属性 "bar",但类型 "Foo" 中需要该属性。ts(2741)
4.ts(3, 5): 在此处声明了 "bar"。
*/
let foo: Foo = {};
复制代码

解决

interface Foo {
  bar: number;
}

let foo: Foo = {
  bar: 1,
};
复制代码

类型兼容性是什么:

当一个类型 Y 可以被赋值给一个类型 X 时,我么你就可以说类型 X 兼容类型 Y

X 兼容 Y : X(目标类型) = Y(源类型)

/*
配置--tyconfig.json:  "strictNullChecks": false,

字符型兼容null类型,null是字符型的子类型
*/
let s: string = "a";

s = null;
复制代码

结构之间兼容:成员少的兼容成员多的 函数之间兼容:参数多的兼容参数少的

类型兼容性的作用:

因为 TS 运行我们把一些类型不同的变量相互赋值,虽然在某种程度上可能产生 不可靠的行为,但**增加了语言的灵活性, ** 类型兼容性多存在于类,函数,接口中

接口兼容性

interface X {
  a: any;
  b: any;
}

interface Y {
  a: any;
  b: any;
  c: any;
}

let xtest: X = { a: 1, b: 2 };
let ytest: Y = { a: 1, b: 2, c: 3 };

xtest = ytest;
// 类型 "X" 中缺少属性 "c",但类型 "Y" 中需要该属性。ts(2741)
// ytest = xtest;
复制代码

ytest 可以复制给 xtest, 但 xtest 不能复制给 ytest.Y 接口具备 X 接口的所有属性,不管是否有额外的属性,那么 Y 可以被认为是 X 类型,即 X 类型兼容 Y 类型----鸭式辩型法 成员少的兼容成员多的

函数兼容性

两个函数是否兼容,一般发生在两个函数相互赋值的情况下,如函数作为参数的时候

目标函数, 目标类型

源函数,源类型

目标函数兼容源函数,需要满足三个条件

参数个数限制:

目标函数的参数个数,一定要多于源函数的参数个数

情况一:函数中的参数个数是固定的
type Handler = (a: number, b: number) => void;

function hof(handler: Handler) {
  return handler;
}
/*
当给 hof传入参数时,就会判断是否和 Handler类型兼容,
Handler就是目标型

给 hof传入的参数就是源类型

*/

let handler1 = (a: number) => {};
hof(handler1);

let handler2 = (a: number, b: number, c: number) => {};
/*
类型“(a: number, b: number, c: number) => void”的参数不能赋给类型“Handler”的参数。
因为目标类型--Handler只有连个参数
*/
// hof(handler2);
复制代码
情况二: 函数中具有不固定个数的参数

如果函数中具有不固定个数的参数 可选参数和剩余参数

固定参数可以兼容可选参数和剩余参数
let a = (p1: number, p2: number) => {};
let b = (p1?: number, p2?: number) => {};
let c = (...args: number[]) => {};

// a可以兼容b
a = b;
// a可以兼容c
a = c;
复制代码
可选参数不能兼容固定参数和剩余参数
/*
配置 tsconfig.js

 "strictNullChecks": true,
 "strictFunctionTypes": true,
*/

let a = (p1: number, p2: number) => {};
let b = (p1?: number, p2?: number) => {};
let c = (...args: number[]) => {};

// 参数“args”和“p1” 的类型不兼容
b = c;
//  参数“p1”和“p1” 的类型不兼容。
b = a;
复制代码
剩余参数可以兼容固定参数和可选参数
let a = (p1: number, p2: number) => {};
let b = (p1?: number, p2?: number) => {};
let c = (...args: number[]) => {};

c = a;
c = b;
复制代码

参数类型--参数类型一定要匹配

基础类型
type Handler = (a: number, b: number) => void;
function hof(handler: Handler) {
  return handler;
}

let handlers = (c: string) => {};
// 参数“c”和“a” 的类型不兼容
hof(handlers);
复制代码
对象类型
interface Point3D {
  x: number;
  y: number;
  z: number;
}

interface Point2D {
  x: number;
  y: number;
}

let p3d = (points3d: Point3D) => {};
let p2d = (points2d: Point2D) => {};

/*
结果: 
在 函数的 参数是对象类型的时候, 成员多的兼容成员少的

与接口的兼容性正好相反(成员少的兼容成员多的)
*/
p3d = p2d;
// 参数“points3d”和“points2d” 的类型不兼容
p2d = p3d;
复制代码

返回值类型

目标类型函数的返回值必须与源函数的返回值类型相同,或者是 源函数返回值类型的子类型

let f = () => ({ name: "a" });

let g = () => ({ name: "b", age: 20 });

// f兼容g
f = g;
// g不兼容f
g = f;
复制代码

因为 f 的返回值类型是 g 返回值类型的子类型,而且这里也是成员少的兼容成员多的(与鸭式辩型法一致)

函数重载

函数重载包括 重载列表和函数实现 重载列表中的函数就是目标函数, 函数的实现就是源函数

程序在运行时,会查找重载列表,

在重载列表中,目标函数的参数个数要 多余 源函数的参数个数

// 目标函数
function overloadtest(a: number, b: number, c: number): number;
function overloadtest(a: string, b: string, c: string): string;
// 源函数
function overloadtest(a: any, b: any): any {}
复制代码
function overloadtest(a: number, b: number, c: number): number;
function overloadtest(a: string, b: string, c: string): string;
// 会出现错误-原因: 参数过多
function overloadtest(a: any, b: any, c: any, d: any): any;
{
}
复制代码
// 会出现错误-原因: 返回值不兼容
function overloadtest(a: number, b: number, c: number): number;
function overloadtest(a: string, b: string, c: string): string;
//
function overloadtest(a: any, b: any, c: any) {}
复制代码

枚举类型的兼容性

枚举类型和数字类型可以相互兼容

enum Fruit {
  Apple,
  Banana,
}

enum Color {
  Red,
  Yellow,
}

let fruit: Fruit.Apple = 3;

let numtest: number = Fruit.Apple;
复制代码

枚举类型互不兼容

enum Fruit {
  Apple,
  Banana,
}

enum Color {
  Red,
  Yellow,
}

// 不能将类型“Fruit.Apple”分配给类型“Color.Red”
let color: Color.Red = Fruit.Apple;
复制代码

类的兼容性

在比较两个类是否兼容时, 静态成员和构造函数式不参与比较的,如果两个类具有相同的实例成员,那么他们的实例就会相互兼容

class A {
  constructor(p: number, q: number) {}
  id: number = 1;
}

class B {
  static s = 1;
  constructor(p: number) {}
  id: number = 2;
}

let aa = new A(1, 2);

let bb = new B(1);

aa = bb;

bb = aa;
复制代码

如果类中含有私有成员,只有父类和子类是可以相互兼容的

class A {
  constructor(p: number, q: number) {}
  id: number = 1;
  private name: string = "";
}

class B {
  static s = 1;
  constructor(p: number) {}
  id: number = 2;
  private name: string = "";
}

let aa = new A(1, 2);

let bb = new B(1);
// 不能将类型“B”分配给类型“A”。 类型具有私有属性“name”的单独声明
aa = bb;
// 不能将类型“B”分配给类型“A”。 类型具有私有属性“name”的单独声明
bb = aa;
复制代码
class A {
  constructor(p: number, q: number) {}
  id: number = 1;
  private name: string = "";
}

class C extends A {}

let aa = new A(1, 2);
let cc = new C(1, 2);

aa = cc;
cc = aa;
复制代码

泛型的兼容性

泛型变量的兼容性

// 泛型T 被接口使用后就会影响泛型的兼容性
interface Empty<T> {
  value: T;
}

let objtest1: Empty<number> = {};
let objtest2: Empty<string> = {};

// 不兼容
objtest1 = objtest2;
复制代码

泛型函数的兼容性

如果两个泛型函数的定义相同,而且没有指定类型参数, 二者是相互兼容的

let log1 = <T>(x: T): T => x;

let log2 = <U>(x: U): U => x;

log1 = log2;

log2 = log1;
复制代码
enum Type {
  Strong,
  Week,
}

class java {
  hellojava() {
    console.log("hello java");
  }
}

class javascript {
  hellojavascript() {
    console.log("hello javascript");
  }
}

function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new java() : new javascript();
  /*
    类型“java | javascript”上不存在属性“hellojava”。
  类型“javascript”上不存在属性“hellojava”。ts(2339)
    */
  if (lang.hellojava) {
    lang.hellojava();
  } else {
    /*
        类型“java | javascript”上不存在属性“hellojava”。
  类型“javascript”上不存在属性“hellojava”。ts(2339)
        */
    lang.hellojavascript();
  }

  return lang;
}

getLanguage(Type.Strong);
复制代码

解决方式一(类型断言)

......
function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java : new Javascript;
    if(!!(lang as Java).helloJava) {
        (lang as Java).helloJava();
    } else {
        (lang as Javascript).hellojavascript();
    }
    return lang;
}
......
复制代码

代码可读性差,而"类型保护机制" 可以解决这个问题,因为它可以提前对类型做出预判

类型保护是什么

typescript 能够在特定区块中保证变量属于某种确定类型。可以在此区块中放心地引用此类型的属性,或者调用此类型的方法

使用方式一(instanceof)

......
function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java : new Javascript;
		// instanceof判断 是否属于某个类
    if(lang instanceof Java){
        lang.helloJava();
    }else{
        lang.hellojavascript();
    }
    return lang;
}

......
复制代码

使用方式二(in)

......
function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java : new Javascript;
		// in 判断是否属于某个对象
    if('helloJava' in lang) {
        lang.helloJava();
    }else{
        lang.hellojavascript();
    }
    return lang;
}
......
复制代码

使用方式三(typeof)

let x: number | string = "";
//  typeof 判断基本类型
if (typeof x === "string") {
  // 在此区块中是 string 类型,就拥有string类型的一些属性/方法
  console.log(x.length);
} else if (typeof x === "number") {
  // 在此区块中是 number 类型,就拥有number类型的一些属性/方法
  console.log(x++);
}
复制代码

使用方式四(类型保护函数)

创建类型保护函数来判断对象的类型

enum Type {
  Strong,
  Week,
}

class Java {
  helloJava() {
    console.log("hello java");
  }
}

class Javascript {
  hellojavascript() {
    console.log("hello javascript");
  }
}

function isJava(lang: Java | Javascript): lang is Java {
  return (lang as Java).helloJava !== undefined;
}

function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java() : new Javascript();

  if (isJava(lang)) {
    lang.helloJava();
  } else {
    lang.hellojavascript();
  }
  return lang;
}

getLanguage(Type.Strong);
复制代码
文章分类
前端
文章标签