作者:王春雨

前言

随着前端工程化的快速发展, TypeScript 变得越来越受欢迎,它已经成为前端开发人员必备技能。 TypeScript 最初是由微软开发并开源的一种编程语言,自 2012 年 10 月发布首个公开版本以来,它已得到了人们的广泛认可。TypeScript 发展至今,已经成为很多大型项目的标配,其提供的静态类型系统,大大增强了代码的可读性、可维护性和代码质量。同时,它提供最新的 JavaScript 特性,能让我们构建更加健壮的组件,新版本不断迭代更新,编写前端代码也越来越香。

typescript 下载量变化趋势(来自于 npm trends)

1 为什么使用 TypeScript

微软提出 TypeScript 主要是为了实现两个目标:为 JavaScript 提供可选的类型系统,兼容当前及未来的 JavaScript 特性。首先类型系统能够提高代码的质量和可维护性,国内外大型团队经过不断实践后得出一些结论:

像其他语言都有类型的存在,如果强加于 JavaScript 之上,类型可能会有一些不必要的复杂性,而 TypeScript 在两者之间做了折中处理尽可能地降低了入门门槛,它使 JavaScript 即 TypeScript ,为 JavaScript 提供了编译时的类型安全。TypeScript 类型完全是可选的,原来的 .js 文件可以直接被重命名为 .ts ,ts 文件可以被编译成标准的 JavaScript 代码,并保证编译后的代码全部兼容,它也被成为 JavaScript 的 “超集”。没有类型的 JavaScript 语法虽然简单灵活,使用的变量是弱类型,但是比较难以掌握,TypeScript 提供的静态类型检查,很好的弥补了 JavaScript 的不足。

TypeScript 类型可以是隐式的也可以是显式的,它会尽可能安全地推断类型,以便在代码开发过程中以极小的成本为你提供类型安全,也可以使用显式的声明类型注解让编译器编译出我们想要的内容,更重要的是为下一个必须阅读代码的开发人员理解代码逻辑。

类型错误也不会阻止 JavaScript 的正常运行,为了方便把 JavaScript 代码迁移到 TypeScript,即使存在编译错误,TypeScript 也会被编译出完整的 JavaScript 代码,这与其他语言的编译器工作方式有很大不同,这也正是 TypeScript 被青睐的另一个原因。

TypeScript 的特点还有很多比如下面这些:

  1. 免费开源,使用 Apache 授权协议;
  2. 基于 ECMAScript 标准进行拓展,是 JavaScript 的超集;
  3. 添加了可选静态类型、类和模块;
  4. 可以编译为可读的、符合 ECMAScript 规范的 JavaScript;
  5. 成为一款跨平台的工具,支持所有的浏览器、主机和操作系统;
  6. 保证可以与 JavaScript 代码一起使用,无须修改(这一点保证了 JavaScript 项目可以向 TypeScript 平滑迁移);
  7. 文件扩展名是 ts/tsx;
  8. 编译时检查,不污染运行时;

总的来说我们没有理由不使用 TypeScript, 因为 JavaScript 就是 TypeScript,TypeScript 可以让 JavaScript 更美好。

2 开始使用 TypeScript

2.1 安装 TypeScript 依赖环境

TypeScript 开发环境搭建非常简单,大部分前端工程都集成了 TypeScript 只需安装依赖增加配置即可。所有前端项目都离不开 NodeJS 和 npm 工具,npm 命令安装 TypeScript,通常 TypeScript 自带的 tsc 并不能直接运行 TypeScript 代码,因此我们还会安装 TypeScript 的运行时 ts-node:

npm install --save-dev typescript ts-node

2.1.1 集成 Babel

前端工程大都离不开 Babel ,我们需要将 TypScript 和 Babel 结合使用,TypeScript 编译器负责对代码进行静态类型检查,Babel 负责将 TypeScript 代码转译为可以执行的 JavaScript 代码:

 

 

Babel 与 TypeScript 结合的关键依赖 @babel/preset-typescript,它提供了从 TypeScript 代码中移除类型相关代码(如,类型注解,接口,类型文件等),并在 babel.config.js 文件添加配置选项:

npm install -D @babel/preset-typescript

// babel.config.js
{
"presets": [
// ...
"@babel/preset-typescript"
]
}

2.1.2 集成 ESlint

代码检查是项目的重要组成部分,TypeScript 自身的约束相对简单只可以发现一些代码错误并不会帮助我们统一代码风格,当项目越来越庞大,开发人员越来越多时,代码风格的约束还是必不可少的。我们可以借助 ESLint 对代码风格进行约束,为了让 eslint 来解析 TypeScript 代码我们需要安装解析器 @typescript-eslint/parser 和 插件 @typescript-eslint/eslint-plugin:

npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

注意: @typescript-eslint/parser 和 @typescript-eslint/eslint-plugin 必须使用相同的版本

在 .eslintrc.js 配置文件中添加选项:

 "parser": "@typescript-eslint/parser",
      "plugins": ["@typescript-eslint"],


// 可以直接启用推荐的规则
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
]
// 也可以选择自定义规则
"rules": {
"@typescript-eslint/no-use-before-define": "error",
// ...
}

自定义规则选项具体解读:

2.2 配置 TypeScript

TypeScript 本身提供了只使用参数在命令行编译 TypeScript 文件,但是在实际项目开发时我们都会使用 tsconfig.json ,如果项目中没有此文件,可以手动创建也可以使用命令行创建(tsc —init)。使用 TypeScript 初期仅需要一份默认的 tsconfig.json 即可,它包含了一下基本的编译选项相关信息,当我们需要定制编译选项时就需要去了解每一项具体的含义,编译选项解读如下:

2.严格的类型检查选项:

3.模块解析选项:

4.Source Map 选项:

5.其它选项:

6.还可以使用 include 和 exclude 选项来指定编译器需要和不需要编译的文件,一般增加必要的 exclude 文件会提升编译性能:

 "exclude": [
    "node_modules",
    "dist"
...
  ],

2.3 TypeScript 类型注解

熟悉了 TypeScript 的相关配置,再来看一看 TypeScript 提供的基本类型,下图是与 ES6 类型的对比:



图中蓝色的为基本类型,红色为 TypeScript 支持的特殊类型

TypeScript 的类型注解相当于其它语言的类型声明,可以使用 let 和 const 声明一个变量,语法如下:

// let 或 const 变量名:数据类型 = 初始值;
//例如:
let varName: string = 'hello typescript'

函数声明,推荐使用函数表达式,也可以使用箭头函数显得更简洁一下:

let 或 const 函数表达式名 = function(参数1:类型,参数2:类型):类型{
// 执行代码
// return xx;
}
// 例如
let sum = function(num1: number, num2: number): number {
return num1 + num2;
}

2.4 TypeScript 特殊类型介绍

typescript 基本类型的用法和其它后端语言类似在这里不进行详细介绍,TypeScript 还提供了一些其它语言没有的特殊类型在使用过程中有很多需要注意的地方。

2.4.1 any 任意值

any 在 TypeScript 类型系统中占有特殊的地位。它为我们提供了一个类型系统的 “后门”,TypeScript 会把类型检查关闭,它能够兼容所有的类型,因此所有类型都能被赋值给它。但我们必须减少对它的依赖,因为需要确保类型安全,除非必须使用它才能解决问题,当使用 any 时,基本上是在告诉 TypeScript 编译器不用进行任何类型检查。

任意值类型和 Object 有相似的作用,但是 Object 类型的变量只允许给它赋值不同类型的值,但是却不能在它上面调用方法,即便真有这些方法:

2.4.2 void、null 和 undefined

空值(void)、null 和 undefined 这几个值类似,在使用的过程中很容易混淆,以下依次进行说明:

2.4.3 枚举

TypeScript 语言支持枚举类型,它是对 JavaScript 标准数据类型的一个补充。枚举取值被限定在一定范围内的场景,在实际开发中有很多场景都适合用枚举来表示,枚举类型可以为一组数据赋予更加友好的名称,从而提升代码的可读性,使用 enum 关键字来定义:

enum SendType {
SEND_NORMAL,
SEND_BATCH,
SEND_FRESH,
...
}
console.log(SendType.SEND_NORMAL === 0) // true
console.log(SendType.SEND_BATCH === 1) // true
console.log(SendType.SEND_FRESH === 2) // true

一般枚举的声明都采用首字母大写或者全部大写的方式,默认枚举值是从 0 开始编号。也可以手动编号为数值型或者字符串类型:

// 数值枚举
enum SendType {
SEND_NORMAL = 1,
SEND_BATCH = 2,
SEND_FRESH,  // 按以上规则自动赋值为 3
...
}
const sendtypeVal =  SendType.SEND_BATCH; 

// 编译后输出代码
var SendType;
(function (SendType) {
    SendType[SendType["SEND_NORMAL"] = 1] = "SEND_NORMAL";
    SendType[SendType["SEND_BATCH"] = 2] = "SEND_BATCH";
    SendType[SendType["SEND_FRESH"] = 3] = "SEND_FRESH"; // 按以上规则自动赋值为 3
})(SendType || (SendType = {}));

var sendtypeVal =  SendType.SEND_BATCH; 

// 字符串枚举
enum PRODUCT_CODE {
  P1 = 'ed-m-0001', // 特惠送
  P2 = 'ed-m-0002', // 特快送
  P4 = 'ed-m-0003', // 同城即日
  P5 = 'ed-m-0006', // 特瞬送城际
}

这样写法编译后的常量代码比较冗长,而且在运行时 sendtypeVal 的取值不变,将会查找变量 SendType 和 SendType.SEND_BATCH。我们还有一个可以使代码更简洁且能获得性能提升的小技巧那就是使用常量枚举(const enum)。

// 使用常量枚举编译前
const enum SendType {
    SEND_NORMAL = 1,
    SEND_BATCH = 2,
    SEND_FRESH  // 按以上规则自动赋值为 3
}

const sendtypeVal =  SendType.SEND_BATCH;

// 编译后
var sendtypeVal = 2 /* SendType.SEND_BATCH */;

2.4.4 never 类型

大多数情况我们并不需要手动定义 never 类型,只有在写一些非常复杂的类型和类型工具方法,或者为一个库定义类型等情况下才需要用到它,never 类型一般出现在函数抛出异常或存在无法正常结束的情况下。

2.4.5 元组类型

元组类型的声明和数组比较类似,只是元组中的各个元素类型可以不同。简单示例如下:

// 元祖示例
let row: [number, string, number] = [1, 'hello', 88];

2.4.6 接口 interface

接口是 TypeScript 的一个核心概念,它能将多个类型声明组合成一个类型注解:

interface CountDown {
readonly uuid: string // 只读属性
  time: number
  autoStart: boolean
  format: string
value: string | number // 联合类型,支持字符串和数值型
[key: string]: number // 字符串的键,数值型的值
}
interface CountDown {
  finish?: () => void // 可选类型
  millisecond?: boolean // 可选方法
}
// 接口可以重复声明,多次声明可以合并为一个接口

接口可以继承其它类型对象,相当于将继承的对象类型复制到当前接口:

interface Style {
color: string
}
interface: Shape {
name: string
}
interface: Circle extends Style, Shape {
radius: number

// 还会包含继承的属性
// color: string
// name: string
}
const circle: Circle = { // 包含 3 个属性
radius: 1,
color: 'red',
name: 'circle'
}

如果子接口与父接口之间存在同名的类型成员,那么子接口中的类型成员具有更高优先级。

2.4.7 类型别名 type

TypeScript 提供了为类型注解设置别名的便捷方法——类型别名,类型别名就是可以给一个类型起一个新名字。在 TypeScript 中使用关键字 type 来描述类型变量:

type StrOrNum = string | number
// 用法和其它基本类型一样
let sample: StrOrNum
sample = 123
sample = '123'
sample = true // 错误

与接口区别,我们可以为任意类型注解设置别名,这在联合类型和交叉类型中比较实用,下面是一些常用方法

type Text = string | { text: string } // 联合类型

type Coordinates = [number, number] // 元组类型
type Callback = (data: string) => void // 函数类型

type Shape = { name: string } // 对象类型
type Circle = Shape & { radius: number} // 交叉类型,包含了 name 和 radius 属性

如果需要使用类型注解的层次结构,请使用接口,它能使用 implements 和 extends。为一个简单的对象类型使用类型别名,只需要给它一个语义化的名字即可。另外,想给联合类型和交叉类型提供一个语义化的别名时,使用类型别名更加合适而不是用接口。类型别名与接口的区别如下:

  1. 类型别名能够表示非对象类型,接口则只能表示对象类型,因此我们想要表示原始类型、联合类型和交叉类型时只能使用类型别名;
  2. 类型别名不支持继承,接口可以继承其它接口、类等对象类型,类型别名可以借助交叉类型来实现继承的效果;
  3. 接口名总是会显示在编译器的诊断信息和代码编辑器的智能提示信息中,而类型别名的名字只在特定情况下显示;
  4. 接口具有声明合并的行为,而类型别名不会进行声明合并;

2.4.8 命名空间 namespace

随着项目越来越复杂,我们需要一种手段来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突。因此我们把一些代码放到一个命名空间内,而不是把它们放到全局命名空间下。现实生活中,一个学校里经常会出现同名同姓的同学,如果在不同班里,就可以用班级名 + 姓名来区分。其实命名空间与班级名的作用一样,可以防止同名的函数和变量相互影响。

TypeScript 中命名空间使用 namespace 关键字来定义,基本语法格式:

namespace 命名空间名 {
const 私有变量; 
export interface 接口名;
export class 类名;
}
// 如果需要在命名空间外部调用需要添加 export 关键字
命名空间名.接口名;
命名空间名.类名;
命名空间名.私有变量; // 错误,私有变量不允许访问

在构建比较复杂的应用时,往往需要将代码分离到不同的文件中,以便进行维护,同一个命名空间可以出现在多个文件中。尽管是不同的文件,但是它们依然是同一个命名空间,使用时就如同它们在一个文件中定义的一样。

// 多文件命名空间
// Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}

// NumberValidator.ts
namespace Validation { // 相同命名空间
export interface NumberValidator {
isAcceptable(num: number): boolean;
}
}

2.4.9 泛型

TypeScript 设计泛型的关键动机是在成员之间提供有意义的类型约束,这些成员可以是类的实例成员、类的方法、函数的参数、函数的返回值。使用泛型,可以将相同的代码用于不同的类型(语法:一般在类名、方法名的后面加上<泛型> ),一个队列的简单实现与泛型的示例:

class Queue {
private data = []
push = item => this.data.push(item)
pop = () => this.data.shift()
}

const queue = new Queue()
// 在没有约束的情况下,开发人员很可能进入误区,导致运行时错误(或潜在问题)
queue.push(0) // 最初是数值类型
queue.push('1') // 有人添加了字符串类型

// 使用过程中,走入了误区
console.log(queue.pop().toPrecision(1));
console.log(queue.pop().toPrecision(1)); // 运行时错误

一个解决办法可以解决以上问题:

class QueueOfNumber {
private data: number[] = []
push = (item: number) => this.data.push(item)
pop = (): number => this.data.shift()
}
const queue = new Queue()

queue.push(0) 
queue.push('1') // 错误,不能放入一个 字符串类型 的数据

这么做如果需要一个字符串的队列,怎么办?需要重写一遍类似的代码?这时就可以用到泛型,可以让放入的类型和取出的类型一样:

class Queue<T> {
private data: T[] = []
push = (item: T) => this.data.push(item)
pop = (): T | undefined => this.data.shift()
}
// 数值类型
const queue = new Queue<number>()
queue.push(0) 
queue.push(1) 
// 或者 字符串类型
const queue = new Queue<string>()
queue.push('0')
queue.push('1')

我们可以随意指定泛型的参数类型,一般使用简单的泛型时,常用 T、U、V 表示。如果在我们的参数里,拥有不止一个泛型,就应该使用更加语义化的名称,如 TKey 和 TValue。依照惯例,以 T 作为泛型的前缀,在其它语言已经是约定俗成的方式了。

2.4.10 类型断言

TypeScript 程序中的每一个表达式都具有某种类型,编译器可以通过类型注解或类型推导来确定表达式类型,但有时,开发者比编译器更清楚某个表达式的类型,因此就需要用到类型断言,类型断言(Type Assertion)可以用来手动指定一个值的类型,告诉编译器应该是什么类型,具体语法如下:

type AddressVO = { address: string }
(<AddressVO>sendAddress).address // <T> 类型断言
(sendAddress as AddressVO).address // as 类型断言

let val = true as const // 等于 const val = true
function getParams(router: { params: Array<string> } | undefined) {
if(!router) return ''

return router!.params // 告诉编译器 router 是非空的
}

3 深入 TypeScript 泛型编程

泛型编程是一种编程风格或者编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。刚开始进行 TypeScript 开发时,我们很容易重复的编写代码,通过泛型,我们能够定义更加通用的数据结构和类型。许多编程语言都很流行面向对象编程,可以创建公共接口的类并隐藏实现细节,让类之间进行交互,可以有效管理复杂度对复杂领域分而治之。但是对于前端来说泛型编程可以更好的解耦、组件化和可复用。接下来使用泛型处理一种常见的需求:通过示例创建独立的、可重用的组件。

3.1 解耦关注点

我们需要一个 getNumbers 函数返回一个数字数组,允许在返回数组之前对每一项数字应用一个变换处理函数,该函数接收一个数字返回一个新数字。如果调用者不需要任何处理,可以将只返回其结果的函数作为默认值。

type TransformFunction = (value: number) => number

function doNothing(value: number): number ( // doNothing() 只返回原数据,不进行任何处理
  return value
)

function getNumbers(transform: TransformFunction = doNothing): number[] {
    /** */
}

又出现另一种业务场景,有一个 Widget 对象数组,可以从 WidgetWidget 对象创建一个 AssembledWidget 对象。assembleWidgets() 函数处理一个 Widget 对象数组,并返回一个 AssembledWidget 对象数组。因为我们不想做不必要的封装,所以 assembleWidgets() 将一个 pluck() 函数作为实参,给定一个 Widget 对象数组时,pluck() 返回该数组的一个子集。允许调用者告诉函数需要哪些字段,从而忽略其余字段。

type PluckFunction = (widgets: Widget) => Widget[]

function pluckAll(widgets:  Widget[]):  Widget[] ( 
// pluckAll() 返回全部,不进行任何处理
  return widgets
)

// 如果用户没有提供 pluck() 函数,则返回 pluckAll 作为实参的默认值
function assembleWidgets(pluck: PluckFunction = pluckAll): AssembledWidget[] {
    /** */
}

仔细观察可以两处代码都有相似之处,doNothing() 和 pluckAll() 它们都接收一个参数,并不做处理就返回。它们的区别只是接收和返回的值类型不同:doNothing 使用数字,pluckAll 使用 Widget 对象数字,两个函数都是恒等函数。在代数中恒等函数指的是 f(x) = x。在实际开发中这种恒等函数会有很多,出现在各处,我们需要编写一个可重用的恒等函数来简化代码,使用 any 类型是不安全的它会绕过正常的类型检查,这时我们就可以使用泛型恒等函数:

function identity<T>(value: T):  T ( // 有一个类型参数 T 的泛型恒等函数
  return value
)
// 可以使用 identity 代替 doNothing 和 pluckAll

采用这种实现方式,可以将恒等逻辑与实际业务逻辑问题进行更好的解耦,恒等逻辑可以完全独立出来。这个恒等函数的类型参数是 T,当为 T 指定了实际类型时,就创建了具体的函数。

泛型类型:是指参数化一个或多个类型的泛型函数、类、接口等。泛型类型允许我们编写能够支持不同类型的通用代码,从而实现高度的代码重用。使用泛型让代码的组件化程度更高,我们可以把这些泛型组件用作基本模块,通过组合它们实现期望的行为,同时在组件之间只保留下最小限度的依赖。

3.2 泛型数据结构

假如我们要实现一个数值二叉树和字符串链表。把二叉树实现为一个或多个结点,每个结点存储一个数值,并引用其左侧和右侧的子结点,这些引用指向结点,如果没有子结点,可以指向 undefined。

class NumberBinaryTreeNode {
  value: number
  left: NumberBinaryTreeNode | undefined
  right: NumberBinaryTreeNode | undefined

  constructor(value: number) {
    this.value = value
  }
}

类似地,我们实现链表为一个或多个结点,每个结点存储一个 string 和对下一个结点的引用,如果没有下一个结点,引用就指向 undefined。

class StringLinkedListNode {
  value: string
  next: StringLinkedListNode | undefined

  constructor(value: string) {
    this.value = value
  }
}

如果工程的其它部分需要一个字符串二叉树或者数值列表我们可以简单的复制代码,然后替换几个地方,复制从来不是一个好选择,如果原来的代码有 Bug,很可能会忘记在复制的版本中修复 Bug。我们可以使用泛型来避免复制代码。

我们可以实现一个泛型的 NumberTreeNode,使其可用于任何类型:

class BinaryTreeNode<T> {
  value: T
  left: BinaryTreeNode<T> | undefined
  right: BinaryTreeNode<T> | undefined

  constructor(value: T) {
    this.value = value
  }
}

实际我们不应该等待有字符串二叉树的新需求才创建泛型二叉树:原始的 NumberBinaryTreeNode 实现在二叉树数据结构和类型 number 之间产生了不必要的耦合。同样,我们也可以把字符串链表替换成泛型的 LinkedListNode:

class LinkedListNode<T> {
  value: string
  next: LinkedListNode | undefined

  constructor(value: string) {
    this.value = value
  }
}

我们要知道,有很成熟的库已经提供了所需的大部分数据结构(如列表、队列、栈、集合、字典等)。介绍实现,只是为了更好的理解泛型,在真实项目中最好不要自己编写代码,可以从库中选择泛型数据结构,去阅读库中泛型数据结构的代码更有助于提升我们的编码能力。一个可以迭代的泛型链表完整实现供参考如下:

type IteratorResult<T> = {
  done: boolean
  value: T
}

interface Iterator<T> {
  next(): IteratorResult<T>
}

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

function* linkedListIterator<T>(head: LinkedListNode): IterableIterator<T> {
  let current: LinkedListNode<T> | undefined = head
  while (current) {
    yield current.value // 在遍历链表过程中,交出每个值
    current = current.next
  }
}

class LinkedListNode<T> implements Iterable<T> {
  value: T
  next: LinkedListNode<T> | undefined

  constructor(value: T) {
    this.value = value
  }

  // Symbol.iterator 是 TypeScript 特有语法,预示着当前对象可以使用 for ... of 遍历
  [Symbol.iterator](): Iterator<T> { 
    return linkedListIterator(this)
  }
}

我们使用了生成器在遍历数据结构的过程中会交出值,所以使用它能够简化遍历代码。生成器返回一个 IterableIterator,所以我们可以直接在 for … of 循环中使用。

以上对泛型编程的介绍只是凤毛菱角,其实泛型编程支持极为强大的抽象和代码可重用性,使用正确的抽象时,我们可以写出简洁、高性能、容易阅读且优雅的代码。

4 TypeScript 注释指令

4.1 常用注释指令

TypeScript 编译器可以通过编译选项设置对所有 .ts 和 .tsx 文件进行类型检查。但是在实际开发中有些代码可能无法避免检查错误,因此 TypeScript 提供了一些注释指令来忽略或者检查某个 JavaScript 文件或者代码片段:

4.3 三斜线指令

三斜线指令是一系列指令的统称,它是从 TypeScript 早期版本就开始支持的编译指令。目前,已经不推荐继续使用三斜线指令了,因为可以使用模块来取代它的大部分功能。简单了解一下即可,它以三条斜线开始,并包含一个 XML 标签,有几种不同的语法:

5 TypeScript 内置工具类型

TypeScript 提供了很多内置的工具类型根据不同的应用场景选择合适的工具可以减轻很多工作,减少冗余代码提升代码质量,下面列举了一些常用的工具:

interface A {
  x: number
  y: number
  z?: string
}
type T0 = Partial<A>
// 等价于 
type T0 = {
    x?: number | undefined;
    y?: number | undefined;
    z?: string | undefined;
}

type T1 = Required<A>
// 等价于
type T1 = {
    x: number;
    y: number;
    z: string;
}

type T2 = Readonly<A>
// 等价于
type T2 = {
    readonly x: number;
    readonly y: number;
    readonly z?: string | undefined;
}

type T3 = Pick<A, 'x'>
// 等价于
type T3 = {
    x: number;
}

type T4 = Omit<A, 'x'>
// 等价于
type T4 = {
    y: number;
    z?: string | undefined;
}

6 TypeScript 提效工具

6.1 TypeScript 演练场

TypeScript 开发团队提供了一款非常实用的在线代码编辑工具——TypeScript 演练场

地址:https://www.typescriptlang.org/zh/play

6.2 JSDoc Generator 插件

如果使用的是 vscode 编辑器直接搜索( JSDoc Generator 插件)插件地址:https://marketplace.visualstudio.com/items?itemName=crystal-spider.jsdoc-generator 安装成功后,使用 Ctrl + Shift + P 打开命令面板,可以进行如下操作可以自动生成带有 TypeScript 声明类型的文档注释:

CodeLens 是一项特别好用的功能,它能够在代码的位置显示一些可操作项,例如:

VSCode 已经内置了 CodeLens 功能,只需要在设置面板开启,找到 TypeScript 对应的 Code Lens 两个相关选项并勾选上:

开启后的效果,出现引用次数,点击 references 位置可以查看哪里引用了:

6.6 接口自动生成 TypeScript 类型

对于前端业务开发来说,最频繁的工作之一就是和接口打交道,前端和接口之间经常出现出入参不一致的情况,后端的接口定义也需要在前端定义相同的类型,大量的类型定义如果都靠手写不仅工作量大而且容易出错。因此,我们需要能够自动生成这些接口类型定义的 TypeScript 代码。VSCode 插件市场就有这样一款插件——Paste JSON as Code 。

插件地址:https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype

安装这个 VSCode 插件可以将接口返回的数据,自动转换成类型定义接口文件。

1.剪贴板转换成类型定义:首先将 JSON 串复制到剪贴板, Ctrl + Shift + P 找到命令:Paste JSON to Types -> 输入接口名称

{"a":1,"b":"2","c":3} // 复制这段 JSON 代码

// Generated by https://quicktype.io
export interface Obj {
  a: number;
  b: string;
  c: number;
}

2.JSON 文件转换类型定义(这个更常用一些):打开 JSON 文件使用 Ctrl + Shift + P 找到命令: Open quicktype for JSON。下图为 package.json 文件生成类型定义的示例:

对应大量且冗长的接口字段一键生成是不是很方便呢!希望这些工具能给每一位研发带来帮助提升研发效率。

7 总结

TypeScript 是一个比较复杂的类型系统,本文只是对其基本用法进行了简要说明和工作中用到的知识点,适合刚开始使用 TypeScript 或者准备使用的研发人员,对于更深层次的架构设计和技术原理并未提及,如果感兴趣的可以线下交流。用好 TypeScript 可以编写出更好、更安全的代码希望对读到本文的有所帮助并能在实际工作中运用。希望本文作为 TypeScript 入门级为读者做一个良好的开端。感谢阅读!!


↙↙↙阅读原文可查看相关链接,并与作者交流