TypeScript 高级类型,你了解几个?

前言

不可否认,如今 TypeScript 已成为一个前端工程师的所需要具备的基本技能。严谨的类型检测,一方面是提高了程序的可维护性健壮性,另一方面也在潜移默化地提高我们的编程思维,即逻辑性

那么,今天我将会通过结合实际开发场景Vue 3.0 源码中的部分类型定义来简单聊聊 TypeScript 中的高级类型。

interface

interface 被用于对所有具有结构的数据进行类型检测。例如,
在实际开发当中,我们会定义一些对象或数组来描述一些视图结构。

定义对象

常见的有对表格的列的定义:

const columns = [
  {key: "username", title: "用户名"},
  {key: "age", title: "年龄"},
  {key: "gender", title: "性别"}
]

而这个时候,我们就可以通过定义一个名为 column 的接口:

interface column {
  key: string,
  title: string
}

// 使用
const columns: column[] = [
  {key: "username", title: "用户名"},
  {key: "age", title: "年龄"},
  {key: "gender", title: "性别"}
]

定义函数

我们平常开发中使用的 axios,它的调用方式会有很多种,例如 axios.request()axios.get()axios.put()。它本质上是定义了这么一个接口来约束 axios 具备这些方法,它看起来会是这样:

export interface Axios {
  request(config: AxiosRequestConfig): AxiosPromise
  get(url: string, config?: AxiosRequestConfig): AxiosPromise
  delete(url: string, config?: AxiosRequestConfig): AxiosPromise
  head(url: string, config?: AxiosRequestConfig): AxiosPromise
  options(url: string, config?: AxiosRequestConfig): AxiosPromise
  post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
  put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
  patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
}

继承

可复用性是我们平常在写程序时需要经常思考的地方,例如组件的封装、工具函数的提取、函数设计模式的使用等等。而对于接口也同样如此,它可以通过继承来复用一些已经定义好的 interfaceclass

这里我们以 Vue 3.0 为例,它在 compiler 阶段,将 AST Element 节点 Node 分为了多种 Node,例如 ForNodeIFNodeIfBranchNode 等等。而这些特殊的 Node 都是继承了 Node

Nodeinteface 接口定义:

export interface Node {
  type: NodeTypes
  loc: SourceLocation
}

IFNodeinterface 接口定义:

export interface IfNode extends Node {
  type: NodeTypes.IF
  branches: IfBranchNode[]
  codegenNode?: IfConditionalExpression
}

可以看到 Node 的接口定义是非常纯净的,它描述了 Node 所需要具备最基本的属性:type 节点类型、loc 节点在 template 中的起始位置信息。而 IFNode 则是在 Node 的基础上扩展了 branchescodegenNode 属性,以及重写了 Nodetype 属性为 NodeTypes.IF

这里简单介绍一下 IFNode 的两个属性:

  • branches 表示它对应的 elseelse if 的节点,它可能是一个或多个,所以它是一个数组。
  • codegenNode 则是 Vue 3.0 的 AST Element 的一大特点,它描述了该节点的一些属性,例如 isBlockpatchFlagdynamicProps 等等,这些会和 runtime 的时候靶向更新和靶向更新密切相关。
近段时间,我也在写一篇关于 Vue 3.0 如何实现 runtime + compile 优雅地实现靶向更新和静态提升的文章,应该会在下周末竣工。

小结

对于 interface 的介绍和使用,我们这里点到即止。当然,它还有很多高级的使用例如结合泛型、定义函数类型等等。有兴趣的同学可以自行去了解这方面的实战。

交叉类型和联合类型

交叉类型

交叉类型故名思意,有着交叉之效。我们可以通过交叉类型来实现多个类型的合并。例如,在 Vue3 中,compile 阶段除了会进行 baseParse 之外,还会进行 transform,这样最后的 AST 才会进行 generate 生成可执行的代码。所以,compile 阶段它就会对应多个 options,即它也是一个交叉类型:

export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

而这个 CompilerOptions 类型别名会用于 baseCompiler 阶段:

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {}
而为什么是多个 options 的交叉,是因为 baseCompiler 只是最基础的 compiler,上面还有更高级的 compiler-domcompiler-ssr 等等。

联合类型

同样地,联合类型其有着合的效果。即有时候,你希望一个变量的类型是 String
或者是 number 的时候,你就可以使用联合类型来约束这个变量。例如:

let numberOrString: number | string
numberOrString = 1
numberOrString = '1'

并且,在实际的开发中使用联合类型,我们通常会遇到这样的提示,例如:

interface student {
  name: string
  age: number
}
interface teacher {
  name: string
  age: number
  class: string
}
let person: student | tearcher = {} as any

person.class = "信息161"
// property 'person' does not exit on type `student`

这个时候,我们可以利用类型断言,告诉 TypeScript 我们知道 person 是什么:

(person as teacher).class = "信息161"

小结

对于交叉类型和联合类型,应该是属于我们平常开发中会使用频繁的部分。并且,从它们的概念上,不难理解,它们本质上就是数学中的并集交集,只不过在基本类型上有着不同的表现而已。

类型保护与区分类型

类型保护

在讲联合类型的时候,我们说了它本质上是交集,这也导致了我们不能直接使用交集之外的属性或方法。所以,我们得通过在不同情况下使用类型断言来告知 TypeScript 它是什么,从而使用交集之外的属性。

但是,频繁地使用类型断言无疑降低了代码的可读性。而针对这一问题,TypeScript 提供了类型保护的机制来减少类型断言,它是一个主谓宾句,例如仍然是上面的例子,我们可以实现这样的类型保护:

function isTeacher(person: Tearcher | Student): person is Teacher {
   return (person).class !== undefined
}

然后通过 isTeacher 这个函数来判断当前类型,再进行相应地属性访问:

if (isTeacher(person)) {
  // 这里访问 teacher 独有的属性
  person.class = "信息162"
} else {
  person.name = "wjc"
}

typeof 与 instanceof

有了类型保护,这个时候我们就会遇到这样的问题:如果我的联合类型中存在多个类型,那么岂不是得定义多个类似 isTeacher 这样的借助类型保护的函数?幸运的是,在 TypeScript 使用 typeofinstanceof 可以自动实现类型保护和区分。

typeof

在 TypeScript 中对基础类型使用 typeof 时,会自动进行类型保护,例如:

function isNumberOrString(param: string | number) {
    if (typeof param === 'string') {
        return param.split('')
    } else {
        return param++
    }
}

instanceof

不同于 typeof 只能对基础类型进行类型保护,instanceof 可以实现对所有类型的类型保护。它是通过构造函数来实现类型保护。

interface Person {
    name: string
    age: string
  }
  class Teacher implements Person {
    name: string
    age: string
    constructor(name: string, age: string) {
        this.name = name
        this.age = age
    }
    teach() {
      console.log("i am a teacher.")
    }
  }
  class Student implements Person {
    name: string
    age: string
    constructor(name: string, age: string) {
        this.name = name
        this.age = age
    }
    study() {
      console.log("i am a student.")
    }
  }
  const person: Student | Teacher = null as any

 if (person instanceof Teacher) {
     person.teach()
 } else {
     person.study()
 }

小结

对于 typeofinstanceof 其实在 JavaScript 中也是老生常谈的知识点,因为传统的类型检测我们会更偏向使用 Object.String.prototype.toString() 来实现。但是,反而在 TypeScript 中,它们两者反而混的如鱼得水。

类型别名

类型别名,我想大家脑海中第一时间想到的就是 Webpack 中的 alias 为路径配置别名。但是,TypeScript 中的类型别名与它只是形同,但是意不同。通俗点讲,就是通过它可以给类型起一个名称:

type age = number
const getAge = () => age

// 等同于
interface Age {
  getAge():number
}

字面量类型

字面量类型是指我们可以通过使用类型别名和联合类型来实现枚举类型,例如:

type method = get | GET | post | POST | PUT | put | DELETE | delete

索引类型与映射类型

对于索引类型和映射类型,这里我们通过 loadash 中常用的一个函数 pluck 来讲解:

在 JavaScript 中实现:

function pluck(object, names) {
  return names.map(name => object[name])
}

在 TypeScript 中实现:

function puck(object: T, names: K[]): T[K][] {
  return names.map(name => object[name])
}

这里我们为 puck 函数定义了两个泛型变量 TK,其中 K 是继承于 T 中所有属性名的类型,所以形参中 names 被约束为 T 中属性的数组,这个过程被称为类型索引。而对于 puck 函数的返回值 T[K][] ,则代表返回的值是一个数组,并且数组值被约束为 T 中属性值为 K 的值,这个过程被称为索引访问

理解这两种概念可能会有点晦涩,但是对于每一者都分开去理解过程会比较有逻辑性。

写在最后

虽然,TypeScript 已成为一项前端工程师的必备技能。但是,相信很多小伙伴还是用的 Javascript 比较多。所以,可能会存在困扰,我该如何提高 TypeScript 编程能力?其实,这个问题很简单,开源的时代,今天我们很多问题都可以通过阅读一些开源的项目源码来解决。这里,我推荐大家可以尝试着去阅读 Vue3.0 的源码,相信通过阅读,你的 TypeScript 编程能力会有质的飞跃。

写作不易,如果你觉得有收获的话,可以帅气三连击!!!

参考资料:

)

往期文章回顾

你可能感兴趣的:(前端,typescript,源码)