详解 Typescript 断言、类型守卫与自定义守卫

一. 断言

类型断言定义:把两种能有重叠关系的数据类型进行相互转换的一种 TS 语法,把其中的一种数据类型转换成另外一种数据类型。类型断言和类型转换产生的效果一样,但语法格式不同。

TS 类型断言语法格式:

A 数据类型的变量  as  B 数据类型  //A 数据类型和 B 数据类型必须具有重叠关系

重要细节:理解重叠关系,以下几种场景都属于重叠关系

  1. 如果  A,B 如果是类并且有继承关系

【 extends 关系】无论 A,B 谁是父类或子类, A 的对象变量可以断言成 B 类型,B 的对象变量可以断言成A类型 。但注意一般在绝大多数场景下都是把父类的对象变量断言成子类。

class Parent {
  public name: string = 'haha'
  public age: number = 123
}


class Son extends Parent {
  public nameSon: string = 'mumu'
  public ageSon: number = 456
}

let p = new Parent()

let res = p as Son // 类型断言
let res = <Son>p // 类型转换
console.log(res.nameSon) // mumu

详解 Typescript 断言、类型守卫与自定义守卫

  1. 如果 A,B 如果是类,但没有继承关系

两个类中的任意一个类的所有的 public 实例属性【不包括静态属性】加上所有的 public 实例方法和另一个类的所有 public 实例属性加上所有的 public 实例方法完全相同或是另外一个类的子集,则这两个类可以相互断言,否则这两个类就不能相互断言。

class Parent {
  constructor(public name: string, public age: number, public address: string) { }
}

class Son {
  public name!: string
  public age!: number
  public address!: string
  public phone: number

  constructor(name: string, age: number, address: string, phone: number) {
    this.address = address
    this.phone = phone
  }
}

let p = new Parent('haha', 18, 'addr')
// 断言成功 Parent为Son的子集
// 如果重叠的属性/方法不为public,则也无法断言成功
let res = p as Son // 正确
console.log(res.phone)
  1. 如果 A 是类,B 是接口,并且 A 类实现了 B 接口【implements】

则 A 的对象变量可以断言成 B 接口类型,同样 B 接口类型的对象变量也可以断言成A类型 。

interface Parent {
  name: string,
  age: number
}

class Son implements Parent {
  public name!: string
  public age!: number
  public address!: string
  public phone: number

  constructor(name: string, age: number, address: string, phone: number) {
    this.address = address
    this.phone = phone
  }
}

let p: Parent = {
  name: 'hah',
  age: 18,
}
let res = p as Son
let res2 = <Son>p

let p2 = new Son('haha', 19, 'oo', 123)
let res3 = p2 as Parent
let res4 = <Parent>p2
  1. 如果 A 是类,B 是接口,并且 A 类没有实现了 B 接口,则断言关系和第2项的规则完全相同。
  2. 如果 A 是类,B 是 type 定义的数据类型
    【就是引用数据类型,例如 Array, 对象,不能是基本数据类型,例如 string,number,boolean】,并且有 A 类实现了 B type 定义的数据类型【 implements】,则 A 的对象变量可以断言成 B type 定义的对象数据类型,同样 B type 定义的对象数据类型的对象变量也可以断言成 A 类型 。
  3. 如果 A 是类,B 是 type 定义的数据类型,并且 A 类没有实现 B type定义的数据类型,则断言关系和第2项的规则完全相同。
  4. 如果 A 是一个函数上参数变量的联合类型,例如 string |number,那么在函数内部可以断言成 string 或number 类型。
function add(num: number | string) {
  // 可以转化为联合类型之一
  num as number
  num as string
}
  1. 多个类组成的联合类型如何断言?例如:
let vechile: Car | Bus | Trunck

vechile 可以断言成其中任意一种数据类型。 例如

vechile as Car, vechile as Bus , vechile as Trunck
  1. 任何数据类型都可以转换成 any 或 unknown 类型,any 或 unknown 类型也可以转换成任何其他数据类型。

二. 一道题

请编写一个操作对象方法和属性的函数实现以下功能

1 当对象字符串属性有空格时就去掉空格后输出。

2 当遇到对象方法时就执行,其他数据类型的属性一律直接输出

3.只有对象中包含al1owoutput属性时,才允许输出。

4.函数接收到外部传入的nu11,undefined,{3时,直接输出不是一个合法的对象

interface TestInter {
  name: string,
  age: number,
  addr: string,
  allowInput: boolean,
  sayTalk: () => void
}

const testObj: TestInter = {
  name: ' kk   yi ',
  age: 18,
  addr: 'earth',
  allowInput: true,
  sayTalk() {
    console.log(this.name + '吃饭');
  }
}

// 工具类
class StringUtils {
  public static trimSpace(str: string) {
    return str.replace(/\s+/g, '')
  }
}

function processOutput(obj: object) {
  // 判断allowInput是否存在与对象中
  if ('allowInput' in obj) {
    let value
    Object.keys(obj).forEach(key => {
      value = obj[key]
      // 范围缩小到string类型
      if (typeof value === 'string') {
        console.log('key:' + StringUtils.trimSpace(value));
      }
      else if (typeof value === 'function') {
        // 直接调用无法找到this
        // value()
        // 通过对象调用的方式绑定this
        obj[key]()
      }
      else {
        console.log(key + ':' + value);
      }
    })
  }
}
processOutput(testObj)

四. 类型守卫

为什么要用类型守卫:

类型守卫定义:

在 语句的块级作用域【if语句内或条目运算符表达式内】缩小变量的一种类型推断的行为。

类型守卫产生时机

TS  条件语句中遇到下列条件关键字时,会在语句的块级作用域内缩小变量的类型,这种类型推断的行为称作类型守卫 ( Type Guard )。类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型,从而减少不必要的类型断言。

  • 类型判断:typeof
  • 属性或者方法判断:in
  • 实例判断:instanceof
  • 字面量相等判断:==, ===, !=, !==

instanceOf原理

//  instance_of这个函数就是我们用来模拟instanceof功能的函数
  //  它接收两个参数,第一个是检测的对象obj,第二个是检测的构造函数constructor
  function instance_of(obj, constructor) {
    let prototype = constructor.prototype;
    //  这里使用了一个定义在Object上面的API => getPrototypeOf
    //  它用于返回当前对象的原型的值
    let proto = Object.getPrototypeOf(obj);
    while(true) {
      //  如果查找完整个原型链还找不到prototype属性那么就返回null
      if(proto === null) {
        return false;
      } 
      //  这里就是对比当前原型对象上是否含有构造函数的prototype属性来判断结果,是则返回true循环结束
      else if(proto === prototype) {
        return true;
      }
      // 每一次循环都会返回当前对象的原型对象
      // 第一次当然是返回传入参数obj的原型对象,往后就是原型链上的每一个对象
      proto = Object.getPrototypeOf(proto);
    }
  }

使用场景?

class Customer {
  rentVechile(vechile: Bus | Truck | Car) {
    // 是否继承自Car类?
    if(vechile instanceof Car) {
      vechile.check()
    } 
  }
}

五. 多态

1.多态的定义:

父类的对象变量可以接受任何一个子类的对象
从而用这个父类的对象变量来调用子类中重写的方法而输出不同的结果.

2.产生多态的条件:

1.必须存在继承关系 2.必须有方法重写

3.多态的好处:

利于项目的扩展【从局部满足了 开闭原则–对修改关闭,对扩展开放】

4.多态的局限性

无法直接调用子类独有方法,必须结合instanceof类型守卫来解决


class Animal {
  public name!: string
  say() {
    console.log('动物叫');
  }
}

class Dog extends Animal {
  say(): void {
    console.log('汪汪汪~');
  }
}

class Cat extends Animal {
  say(): void {
    console.log('喵喵喵~');
  }
}

class Brid extends Animal {
  say(): void {
    console.log('叽叽叽~');
  }
}


class Customer {
  rentVechile(vechile: Vechile) {
    // 共同方法
    // 在各个类中不同的实现
    return vechile.calculateRent()
  }
}



let p4: Animal = new Animal()
p4.say() // 动物叫
p4 = new Dog()
p4.say() // 汪汪汪~
p4 = new Cat()
p4.say() // 喵喵喵~

六. 抽象类

一个在任何位置都不能被实例化的类就是一个抽象类【abstract class 】

什么样的类可以被定义为抽象类

从宏观上来说,任何一个实例化后毫无意义的类都可以被定义为抽象类。 比如:我们实例化一个玫瑰花类的对象变量,可以得到一个具体的 玫瑰花 实例对象,但如果我们实例化一个 Flower 类的对象变量,那世界上有一个叫 花 的对象吗?很明显没有,所以 Flower 类 可以定义为一个抽象类,但玫瑰花可以定义为具体的类。

一个类定义为抽象类的样子

abstract class {}

可以有 0 到多个抽象方法【只有方法体,没有方法实现的方法】,可以有 0 到多个具体方法,可以有构造器,可以有 0 到多个实例属性,0 到多个静态属性,0 到多个静态方法

单纯从类的定义上来看和普通类没有区别,只是多了可以有 0 到多个抽象方法这一条,并且不能被实例化。
抽象类的特点

可以包含只有方法声明的方法【 和方法签名类似,就是多了 abstract 关键字】,也可以包含实现了具体功能的方法,可以包含构造器,但不能直接实例化一个抽象类,只能通过子类来实例化。

抽象类相比普通类充当父类给项目带来的好处

好处1:提供统一名称的抽象方法,提高代码的可维护性:抽象类通常用来充当父类,当抽象类把一个方法定义为抽象方法,那么会强制在所有子类中实现它,防止不同子类的同功能的方法命名不相同,从而降低项目维护成本。

好处2:防止实例化一个实例化后毫无意义的类。

abstract class People {
  abstract say(): void
}

class Man extends People {
  // 必须实现抽象方法
  public say() {
    console.log('hi');
  }
}


// 无法实例化
let p = new Peoplr() // error

实际使用:

interface MouseListenerProcess {
  mouseReleased(e: any): void//  鼠标按钮在组件上释放时调用。
  mousePressed(e: any): void//  鼠标按键在组件上按下时调用。
  mouseEntered(e: any): void //鼠标进入到组件上时调用。

  mouseClicked(e: any): void// 鼠标按键在组件上单击(按下并释放)时调用。
  mouseExited(e: any): void//  鼠标离开组件时调用。
}
// 适配器Adapter是一个抽象类
abstract class MouseListenerProcessAdapter implements MouseListenerProcess {
  // 不常用的方法由抽象类直接实现
  mouseReleased(e: any): void {
    throw new Error('Method not implemented.');
  }
  mousePressed(e: any): void {
    throw new Error('Method not implemented.');
  }
  mouseEntered(e: any): void {
    throw new Error('Method not implemented.');
  }

  // 常用的方法由子类实现
  abstract mouseClicked(e: any): void;

  abstract mouseExited(e: any): void;
}

class MyMouseListenerProcess extends MouseListenerProcessAdapter {
  mouseClicked(e: any): void {
    throw new Error('Method not implemented.');
  }
  mouseExited(e: any): void {
    throw new Error('Method not implemented.');
  }
}

七. 自定义守卫

自定义守卫格式:

function  函数名( 形参:参数类型【参数类型大多为any】)  : 形参 is A类型 = boolean+类型守卫能力{
  return  true or false
}

理解:返回布尔值的条件表达式赋予类型守卫的能力, 只有当函数返回 true 时,形参被确定为 A 类型

/**
 * 判断是否是字符串的自定义守卫方法
 */
// function isString(str: any): boolean {
// 因为ts是在编译期间进行检查,无法对执行后的结果进行类型收窄
function isString(str: any): str is string {
  return typeof str === "string"
}

function isFunction(data: any): data is Function {
  return typeof data === "function"
}

function processObjOutput(obj: any) {

  if (obj && "allowinput" in obj) {// 判断allowinput属性或者方法在ojb对象中是否存在
    // 防止可以随意进行方法调用
    let value: unknown;
    Object.keys(obj).forEach((key) => {
      value = obj[key];
      //if (typeof value === "string") {//把变量的范围缩小为string类型在语句块内使用该数据类型
      if (isString(value)) {
        console.log(key + ":", StringUtil.trimSpace(value));
      }
      //value.age

      if (isFunction(value)) {
        value();
      }
    })
  } else {
    console.log("不是一个合法的对象。")
  }
}

详解 Typescript 断言、类型守卫与自定义守卫

新特性

1. const 不可改变深入

// 限制只读
const arr = [1, 2, 3, 4, 'haha'] as const
arr[0] = 6 // error 不可更改

// 应用:
// 创建不可更改的参数
// 只读
function add(arr: readonly any[]) {
  // error
  arr[0] = 7
}
add(arr)

2. 可变元组

const [username, age, ...rest]: [string, number, ...any[]] = ['xiaohuanggua', 18, 'haha', 12, true]

console.log(username) // 'xiaohuanggua'
console.log(age) // 18
console.log(rest) // ['haha', 12, true]
// 元组标记
let [username, age, ...rest]: [name_: string, age_: number, ...rest: any[]] = ["wangwu", 23,
  "海口海淀岛四东路3号", "133123333", "一路同行,一起飞", 23, "df"]
console.log("username:", username)//wangwu
console.log("age:", age) //23
console.log("rest:", rest) // ["海口海淀岛四东路3号", "133123333", "一路同行,一起飞", 23, "df"]

3. 深入可变元组

let constnum2 = [10, 30, 40, 60, "abc"]
//错误:不能将类型“(string | number)[]”分配给类型“[number, ...any[]]”。
let [x2, ...y2]: [number, ...any[]] = constnum2// 错误
console.log("x2:", x2)
console.log("y2:", y2)


// 解决方法一
let constnum2 = ["df", 30, 40, 60, "abc"]
//  把元组退化成"数组"
let [x2, ...y2]: [...any[]] = constnum2
console.log("x2:", x2)
console.log("y2:", y2)

// 解决方法二
let constnum3 = [10, 30, 40, 60, "abc"] as const
//  把元组固定
let [x3, ...y3]: readonly [any, ...any[]] = constnum3
console.log("x3:", x3)
console.log("y3:", y3)
// 两者等效
let constnum = [10, 30, 40, 60, "abc"] as const
let constnum: readonly (number | string)[] = [10, 30, 40, 60, "abc"]
let constnum5 = [10, 30, 40, 60, "abc"] as const// readonly等效于as const
function tail(arr: readonly [any, ...any[]]) {
  //arr[0] = 33
  let [first, ...rest] = arr;
  return rest;
}
tail(constnum5)

原文链接:https://juejin.cn/post/7343138527419793443 作者:小黄瓜没有刺

(0)
上一篇 2024年3月7日 上午10:22
下一篇 2024年3月7日 上午10:32

相关推荐

发表回复

登录后才能评论