TypeScript 从零到一,2020 开发必备

开发 前端
从弱类型到强类型,开发更加严谨,是前端发展的必然趋势。TypeScript 作为 JS 的超集,自然也推动着前端开发人员去学习 TS,特别是 Vue 3 出现之后且大部分第三方库都有 TS 编译文件之后。TS 的掌握就渐渐变成一项基础技能。

前言

从弱类型到强类型,开发更加严谨,是前端发展的必然趋势。TypeScript 作为 JS 的超集,自然也推动着前端开发人员去学习 TS,特别是 Vue 3 出现之后且大部分第三方库都有 TS 编译文件之后。TS 的掌握就渐渐变成一项基础技能。

 

[[341310]]

开始 TypeScript

学习准备

开始 TypeScript(以下简称 TS)正式学习之前,推荐做好以下准备:

  • Node 版本 > 8.0
  • IDE(推荐 VS Code,TS 是微软推出的,VS Code 也是微软推出,且轻量。对 TS 代码更友好)

 

TypeScript 从零到一,2020 开发必备

初识 TypeScript

打开 TypeScript 官网可以看到官方对 TS 的定义是这样的

  1. JavaScript and More 
  2. A Result You Can TrustGradual Adoption 

这三个点就很好地诠释了 TypeScript 的特性。在此之前,先来简单体验下 TypeScript 给我们的编程带来的改变。

这是一个 .js 文件代码:

  1. let a = 123a = '123' 

这是 .ts 文件代码:

  1. let b = 123b = '123' 

当我们在 TS 文件中试图重新给 b 赋值的时候,发生了错误,鼠标移动到标红处,系统提示:

  1. Type ·"123"' is not assignable to type 'number' 

原因是什么呢?

答案很简单,在 TS 中所有变量都是静态类型,let b = 123 其实就是 'let b:number = 123'。b 只能是 number 类型的值,不能赋值给其他类型。

TypeScript 的优势

  • TS 静态类型,可以让我们在开发过程中发现问题
  • 更友好的编辑器自动提示
  • 代码语义清晰易懂,协作更方便

配上代码来好好感受下这三个优势带给我们的编程体验有多直观,建议边在编辑器上敲代码。

先上最熟悉的 JS:

  1. function add(data) { 
  2.     return data.x + data.y 
  3. }add()  //当直接这样写,在运行的时候才会有错误告知 
  4. add({x:2,y:3}) 

再上一段 TS 代码(如果对语法有疑问可以先不纠结,后续会有讲解,此处可以先带着疑问)

  1. interface Point { x: number, y: number } 
  2. function tsAdd(data: Point): number { 
  3.     return data.x + data.y 
  4. }tsAdd()  //直接这样写,编辑器有错误提示 
  5. tsAdd({ x: 1,y: 123}) 

当我们在 TS 中调用 data 变量中的属性的时候,编辑器会有想 x、y 属性提示,并且我们直接看函数外部,不用深入,就能知道 data 的属性值。这就是 TS 带给我们相比于 JS 的便捷和高效。

TypeScript 环境搭建

搭建 TypeScript 环境,可以直接在终端执行命令:

  1. npm install -g typescript 

然后我们就可以直接 cd 到 ts 文件夹下,在终端运行:

  1. tsc demo.ts 

tsc 简而言之就是 typescript complaire,对 demo.ts 进行编译,然后我们就可以看到该目录下多了一个同名的 JS 文件,可以直接用 Node 进行编译。

到这里我们就可以运行 TS 文件了,但是这只是一个文件,而且还要先手动编译成 TS 在手动运行 Node,有没有一步到位的命令呢?当然有,终端安装 ts-node:

  1. npm install -g ts-node 

这样我们可以直接运行:

  1. ts-node demo.ts 

来运行 TS 文件,如果要初始化 ts 文件夹,进行 TS 相关配置,可以运行:

  1. tsc --init 

关于相关配置,这里我们先简单提下,后面将会分析常用配置,可以先自行打开 tsconfig.json 文件,简单看下其中的配置,然后带着疑问继续往下看。

再理解下 TypeScript 中的 Type

正式介绍 TS 的语法之前,还需要再把开篇提到的静态类型再来说清楚一些。

  1. const a: number = 123 

之前说过,代码的意思是 a 是一个 number 类型的常量,且类型不能被改变。这里我要说的深层意思是,a 具有 number 的属性和方法,当我们在编辑器调用 a 的属性和方法的时候,编辑器会给我们 number 的属性和方法供我们选择。

 

TypeScript 从零到一,2020 开发必备

TS 不仅允许我们给变量定义基础类型,还可以定义自定义类型:

  1. interface Point { 
  2.     x: number 
  3.     y: number 
  4. const a: Point = { 
  5.     x: 2, 
  6.     y: 3 

把 a 定义为 Point 类型,a 就拥有了 Point 的属性和方法。而我们把 a 定义为 Point 类型之后,a 必须 Point 上 的 x 和 y 属性。这样我们就把 Type 理解的差不多了。

TypeScript 的类型分类

类比于 JavaScript 的类型,TypeScript 也分为基础类型和引用类型。

原始类型

原始类型分为 boolean、number、string、void、undefined、null、symbol、bigint、any、never

JS 中也有的这里就不多解释,主要说下之前没有见过的几种类型,但是需要注意一下的是我们在声明 TS 变量类型的时候都是小写,不能写成大写,大写是表示的构造函数。

void 表示没用任何类型,通常我们会将其赋值给一个没有返回值的函数:

  1. function voidDemo(): void { 
  2.     console.log('hello world'

bigint 可以用来操作和存储大整数,即使这数已经超出了 JavaScript 构造函数 Number 能够表示的安全整数范围,实际场景中使用较少,有兴趣的同学可以自行研究下。

any 指的是任意类型,在实际开发中应该尽量不要将对象定义为 any 类型:

  1. let a: any = 4 
  2. a = '4' 

never 表示永不存在的值的类型,最常见的就是函数中不会执行到底的情况:

  1. function error(message: string): never { 
  2.     throw new Error(message) 
  3.     console.log('永不执行'
  4. }function errorEmitter(): never { 
  5.     while(true){} 

引用类型

对象类型:赋值时,内必须有定义的对象属性和方法

  1. const person: { 
  2.     name: string 
  3.     age: number 
  4. } = { 
  5.     name'aaa' 
  6.     age: 18 

数组类型:数组中每一项都是定义的类型。

  1. const numbers: number[] = [1, 2, 3] 

类类型:可以先不关注写法,后面还会详细讲解。

  1. class Peron {} 
  2. const person: Person = new Person() 

类型的介绍差不多就这么些知识点,先在脑海里有个印象,不懂的地方可以继续带着疑问往下看。

TypeScript 类型注解和推断

之前已经讲过 TypeScript 的类型和它的类型种类,这一小节还是想继续把有关类型的知识讲全,那么就是类型注解和类型推断。

类型注解

  1. let a: number 
  2. a = 123 

上面代码中这种写法就是类型注解,通过显式声明,来告诉 TS 变量的类型:

  1. let b = 123 

这里我们并没有显式声明 b 的类型,但是我们在编辑器中把光标放在 b 上,编辑器会告诉我们它的类型。这就是类型推断。

简单的情况,TS 是可以自动分析出类型,但是复杂的情况,TS 无法分析变量类型,我们就需要使用类型注释。

  1. // 场景一 
  2. function add(first,second) { 
  3.     return first + second 
  4. }const sum = add(1,2) 
  5. // 场景二function add2(first: nnumber,second: number) { 
  6.     return first + second 
  7. }const sum2 = add2(1,2) 

在场景一中,形参 first、second 的类型 TS 推断为 any,且函数的返回值也是推断为 any,因为这种情况下,TS 无法判断类型,传参的时候可能传 number 或者 string 等。

场景二中,即使我们没有定义 sum2 的类型,TS 一样可以推断出 number,这是因为 sum2 是由 first second 求和的结果,所以它一定是 number。

不管是类型推断还是类型注解,我们的目的都是希望变量的类型是固定的,这样不会把 typescript 变成 anyscript。

补充:函数结构中的类型注解。

  1. // 情况一 
  2.  function add({ first }: {first: number }): number { 
  3.     return first 
  4. // 情况二 
  5. function add2({firstsecond}: {first: number, second: number}): number { 
  6.     return first + second 
  7. const sum2 = add({ first: 1, second: 2}) 
  8. const sum2 = add2({ first: 1, second: 2}) 

TypeScript 进阶

配置文件

之前我们提到过,当我们要运行 TS 文件时,执行命令 tsc 文件名 .ts 就可以编译 TS 文件生成一个同名 JS 文件,这个过程是怎么来的呢,或者如果我们想修改生成的文件名和文件目录该怎么办呢?

相信你已经心里有答案了,没错,和 webpack 打包或者 babel 编译一样,TS 也有一个编译配置文件 tsconfig.json。当我们执行ts --init,文件目录下就多了一个 TS 配置文件,TS 编译成 js,就是由 tsconfig 中配置而来。

为了验证下 tsconfig 文件确实会对 TS 文件编译做配置,修改里面的:

  1. "removeComments"true //移除文件中的注释 

然后新建一个 demo.ts 文件:

  1. // 这是一个注释 
  2. const a: number = 123 

执行 tsc demo.ts,打开 demo.js 文件,发现注释并没有被移除,这是怎么回事,配置文件不生效?

真相是这样的,当我们直接执行文件的时候,并不会使用 tsconfig 中的配置,只有我们直接执行 tsc,就会使用 tsconfig 中的配置,直接运行 tsc,你就发现了,amazing!

当运行 tsc 命令的时候,直接会先去找到 tsconfig 配置文件,如果没有做其他改动,会默认编译根目录下的 TS 文件。

如果想编译指定文件,则可以在 compilerOptions 配置项同级增加:

  1. "include": ["./demo.ts"]或者"files": ["./demo.ts"

如果想要不包含某个文件,则可以同上增加:

  1. "exclude": ["./demo.ts"

有关于这一块的更多配置,可以参考 tsconfig 配置文档。

下面再来关注下 compilerOptions 中的属性,由这个英文名就知道,这其实就是指的编译配置的意思。

  1. "compilerOptions": { 
  2.     "increments"true                          // 增量编译,只编译新增加的内容 
  3.     "target""es5",                            // 指定 ECMAScript 目标版本: 'ES5' 
  4.     "module""commonjs",                       // 指定使用模块: 'commonjs''amd''system''umd' or 'es2015' 
  5.     "moduleResolution""node",                 // 选择模块解析策略 
  6.     "experimentalDecorators"true,             // 启用实验性的ES装饰器 
  7.     "allowSyntheticDefaultImports"true,       // 允许从没有设置默认导出的模块中默认导入。 
  8.     "sourceMap"true,                          // 把 ts 文件编译成 js 文件的时候,同时生成对应的 map 文件 
  9.     "strict"true,                             // 启用所有严格类型检查选项 
  10.     "noImplicitAny"true,                      // 在表达式和声明上有隐含的 any类型时报错 
  11.     "alwaysStrict"true,                       // 以严格模式检查模块,并在每个文件里加入 'use strict' 
  12.     "declaration"true,                        // 生成相应的.d.ts文件 
  13.     "removeComments"true,                     // 删除编译后的所有的注释 
  14.     "noImplicitReturns"true,                  // 不是函数的所有返回路径都有返回值时报错 
  15.     "importHelpers"true,                      // 从 tslib 导入辅助工具函数 
  16.     "lib": ["es6""dom"],                      // 指定要包含在编译中的库文件 
  17.     "typeRoots": ["node_modules/@types"], 
  18.     "outDir""./dist",                         // 生成文件目录 
  19.     "rootDir""./src"                          // 入口文件 
  20.   }, 

接口(interface)

接口是用来自定义类型或者为我们的第三方 JS 库做翻译的一种方式。之前的代码中已经使用到了接口,其实就是用来描述类型的。每个人都有姓名和年龄,那我们就会这样去约束 person。

  1. interface Person { 
  2.     name: string 
  3.     age: number 
  4. }let person: Person 

当我们进行这样的类型约束的时候,person 这个对象在初始化的时候就必须要有 name 和 age,初始化有两种方式,再来看下其中的不同支出:

  1. // 承接上面的代码 
  2. // 第一种初始化方式 
  3. person = { 
  4.     name'aaa'
  5.     age: 18 
  6. // 第二种初始化方式 
  7. let p = { 
  8.     name'aaa'
  9.     age: 18, 
  10.     sex: 'male' 
  11. person = p 

第一种方式和第二种方式相比,p 对象中多了一个 sex 属性,然后赋值给了 person,编辑器没有提示错误,但是如果在第一种方式中添加一个 sex 属性则会报错,这是为什么呢?

这是因为,当我们直接赋值(也就是通过第一种方式)的时候,TS 会进行强类型检查,因此必须和接口定义的类型一致才行。

注意我们上面提到一致,一致的意思是,属性名和属性值类型一致,且属性个数不多不少。而当使用第二种方式进行赋值的时候,则会进行弱检查。属性个数一致会较弱,表现在,当属性多了一个的时候,不会有语法错误。

此时我们会产生一个疑问,如果我们想让第一种方式也能做到和第二种方式一样,或者说,每个人年龄和姓名是必须的,但是所在城市 city 是选填的,那该如何呢?我们可以用可选属性描述。

  1. interface Person { 
  2.     name: string 
  3.     age: number 
  4.     city?: string 

如果这样的话,我们在调用 p 属性的时候就可以看到 city 属性可能是 string,也可能是 undefined:

 

TypeScript 从零到一,2020 开发必备

不仅如此,我们还希望,age 属性是不可修改的,readonly 属性自然就派上用场了,当你试图修改定义了 readonly 属性的时候,那么编辑器就会发出警告:

  1. interface Person { 
  2.     name: string 
  3.     readonly age: number 
  4.     city?: string 
  5. }let person: Person = { 
  6.     name'aaa'
  7.     age: 18 
  8. }// person.age = 18 

 

TypeScript 从零到一,2020 开发必备

当然这还没结束,如果有一天,还想再扩展一个接口,是公司职员的接口,但是职员接口类肯定有 Person 类的所有信息,再扩展一个 id,又该如何呢?这时候继承(extends)就上场了。

  1. interface Employee extends Person { 
  2.     id: number 

接口还可以用来约束类,让定义的类必须有某种属性或者方法,这时候关键字就不是 extends,而是 implements。

  1. interface User { 
  2.   name: string  getName(): string}class Student implements User { 
  3.   name = 'aaa' 
  4.   getName() {    return this.name 
  5.   }} 

interface VS type

interface 和 type 作用看起来似乎是差不多的,都是用来定义类型,接下让我们看下它的相同点与不同点。

相同点:

1. 都可以描述对象或函数

  1. interface Person { 
  2.   name: string 
  3.   age: number}type Person1 = {  //type 定义类型有等号 
  4.   name: string 
  5.   age: number}interface getResult {  (value: string): void 
  6. }type getResult1 = (value: string): void 

2. 都可以实现继承

  1. // interface 继承 interface 
  2. interface People extends Person {  sex: string 
  3. }// interface 继承 typeinterface People extends Person1 {  sex: string 
  4. }// type 继承 type 
  5. type People1 = Person1 & {  sex: string 
  6. }// type 继承 interface 
  7. type People1 =  Person & {  sex: string 

不同点:

1. type 可以声明基本类型、联合类型,interface 不行

  1. // 基本类型   
  2. type Person = string 
  3. // 联合类型type User = Person | number 

2. interface 可以类型合并

  1. interface People { 
  2.   name: string 
  3.   age: number 
  4. }interface People { 
  5.   sex: string 
  6. }//People 最终类型为 
  7.   name: string 
  8.   age: number 
  9.   sex: string 

3. interface 可以定义可选和只读属性(之前讲过,这里不再赘述)

接口的基础知识差不多就介绍完了,当然接口在实际开发场景中应用会更复杂,如果你还有很多疑惑,接着往下看,下面的讲解将会解答你的疑惑。

联合类型和类型保护

和其他分享资料不同,我希望每一个知识点都能先让你先有所疑惑,启发你的思考,然后我再慢慢解决你的疑惑,这样我相信你会记忆更加深刻,否则可能将成效见微。

闲话少叙,直接上一段代码:

  1. interface Bird { 
  2.   fly: boolean 
  3.   sing: () => {} 
  4. }interface Dog { 
  5.   fly: boolean 
  6.   bark: () => {} 
  7. }function trainAnimal(animal: Bird | Dog) { 
  8.   // animal.sing() 

上面代码中我定义了两个类型,一个 Bird 类型,一个是 Dog 类型。函数 trainAnimal 的形参接收一个 animal 的参数,这个参数可能是 Bird 类型,也可能是 Dog 类型,这就是联合类型。当在函数中调用的时候,编辑器给的提示只有 fly:

 

TypeScript 从零到一,2020 开发必备

这还真有点东西,但是仔细想想,就觉得只有 fly 没毛病。因为联合类型的 animal 无法确定具体是哪个类型,因此只能提示共有的属性。而独有方法经过联合类型阻隔之后是无法进行语法提示。如果我们强行调用某个类型独有的方法,可以看到编辑器会有错误提示。

 

TypeScript 从零到一,2020 开发必备

如果确实需要使用独有方法,该当如何?

这就需要类型保护了,确实,如果联合类型只能调用共有方法,似乎看起来也用处不是很大,好在有类型保护。类型保护也有好多种,我们分别来介绍下。

1. 类型断言

  1. function trainAnimal(animal: Bird | Dog) { 
  2.     if (animal.fly) { 
  3.         (animal as Bird).sing() 
  4.     } else { 
  5.         (animal as Dog).bark() 
  6.     }} 

上面代码中通过一个 as 关键字实现了类型断言。因为按照逻辑,我们知道,如果有 fly 方法,那么 animal 一定是 Bird 类型,但是编辑器不知道,所以通过 as 告诉编辑器此时 animal 就是 Bird 类型,Dog 类型的确定也是同理。

2. 通过 in 来类型断言,TS 语法检查就能确定参数类型

  1. function trainAnimalSecond(anmal: Bird | Dog ) { 
  2.     if ('sing' in animal) { 
  3.         animal.sing() 
  4.     }} 

3. 通过 typeof 来做类型保护

  1. function add(first: string | number, second: string | number) { 
  2.     if (typeof first === 'string' || typeof second === 'string') { 
  3.         return `first:${first}second:${second}` 
  4.     }    return first + second 

上面代码中如何没有 if 里面的逻辑,直接进行判断,编辑器则会给错,因为如果是数字和字符串相加,则可能存在错误,因此通过 typeof 来确定,当 first 和 second 都是数字的时候,进行相加。

4. 通过 instanceof 来类型保护

  1. class NumberObj { 
  2.     count: number}function addSecond(first: object | NumberObj, second: object | NumberObj) { 
  3.     if (first instanceof NumberObj && second instanceof NumberObj) { 
  4.         return first.count + second.count 
  5.     }} 

在 TS 中,类不仅可以用来实例化对象,也可以用来定义变量类型,当一个对象被一个类定义以后,表明这个对象的值就是这个类的实例,关于类这一块的写法有疑问,可以查阅下 ES7 相关内容,这里不做过多讲解。

从代码中我们可以看出,通过 instanceof 来确定具有联合类型的形参是否是类的类型,当然这里如果要用 instanceof 来判断,我们的自定义类型定义只能用 class。如果是 interface 定义的类型,使用 instanceof 则会报错。

枚举类型

枚举这个概念,我们在 JS 中就已经接触的比较多了,关于概念也不就不做过多的讲解,直接上一段代码。

  1. const Status = { 
  2.     OFFLINE: 0, 
  3.     ONLINE: 1, 
  4.     DELETED: 2 
  5. }function getStatus(status) { 
  6.     if (status == Status.OFFLINE) { 
  7.         return 'offline' 
  8.     } else if (status == Status.ONLINE) { 
  9.         return 'online' 
  10.     } else if (status == Status.DELETED) { 
  11.         return 'deleted' 
  12.     }    return error 

这是我们在 JS 中比较常见的写法,TS 中也有枚举类型,而且比 JS 的更好用。

  1. enum Status { 
  2.     OFFLINE,    ONLINE,    DELETED}// 方式一 
  3. const status = Status.OFFLINE  // 0 
  4. // 方式二 
  5. const status = Status[0]  // OFFLINE 

通过上面的代码可以看出,TS 的枚举类型默认会有赋值,而且写法也很简单。再看方式一和方式二对枚举类型的使用,我们可以看出,TS 枚举类型还支持正反调用。

刚才说到枚举类型默认有值,如果我想改默认值又该如何呢?请看下面的代码:

  1. enum Status { 
  2.     OFFLINE = 3, 
  3.     ONLINE,    DELETED}const status = Status.OFFLINE  // 3 
  4. const status = Status.ONLINE  // 4 
  5. enum Status1 {    OFFLINE = 6, 
  6.     ONLINE = 10, 
  7.     DELETED}const status = Status.OFFLINE  // 6 
  8. const status = Status.ONLINE  // 10 
  9. const status = Status.DELETED  // 11 

由上可以看出,TS 枚举类型支持自定义值,且后面的枚举属性没有赋值的话,会在原来的基础上递增。

上面我们说到 enum 支持双向使用,为什么它如此之秀,怎么灵活呢,我们看下枚举类型编译成 JS 后的代码:

  1. var Status; 
  2. (function (Status) { 
  3.     Status[Status["OFFLINE"] = 6] = "OFFLINE"
  4.     Status[Status["ONLINE"] = 10] = "ONLINE"
  5.     Status[Status["DELETED"] = 12] = "DELETED"
  6. })(Status || (Status = {})) 

函数泛型

泛型在 TS 的开发中使用非常广泛,因此这一节,同样会由浅入深,先看代码:

  1. function result(first: string | number, second: string | number) { 
  2.     return `${first} + ${second}` 
  3. }join('1', 1) 
  4. join(1,'1'

这是我们之前讲过的联合类型,两个参数既可以是数字也可以字符串。

但是现在我有个需求是这样的,如果 first 是字符串,则 second 只能是字符串,同理 first 是数字,则 second。如果不知道泛型,我们只能在函数内部去进行逻辑约定,但是泛型一出手,问题就迎刃而解。

  1. function result<T>(first: T,second: T) { 
  2.     return `${first} + ${second}` 
  3. }join<number>(1,1) 
  4. join<string>('1','1'

通过在函数中定义一个泛型 T(名字可以自定义,一般用 T),这样的话,我们就可以约束 first,second 类型一致,当我们试图调用的时候实参类型不一致的时候,那么编辑器就会报错。

  1. function map<T>(params: T[]) { 
  2.     return params 
  3. }map([1]) 

这种形式也是可以的,虽然调用的时候没有显示定义 T,但是 TS 可以推断出 T 的类型。T[] 是数组一种定义类型的方式,表明数组每个值的类型。

注意:Array 这种形式在 3.4 之后,会有警告。统一使用方括号形式。

这是单一泛型,但实际场景中往往是多个泛型:

  1. function result<T, U>(first: T,second: U) { 
  2.     return `${first} + ${second}` 
  3. }join<number,string>(1,'1'
  4. join(1, '1')  //这种形式也可 

泛型如此之好用,肯定不可能只在函数中使用,因此接下来再来说下类中使用泛型:

  1. class DataManager { 
  2.   constructor(private data: string[] | number[]) {} 
  3.   getItem(index: number): string | number { 
  4.     return this.data[index
  5.   }}const data = new DataManager([1]) 
  6. data.getItem(0) 

DataManager 类中构造函数通过联合类型来定义 data 的类型,这在复杂的业务场景中显然是不可取的,因为如果我们也不确定类型,在传参之前,那么只能写更多的类型或者定义成 any 类型,这就显得很不灵活,这时候我们想到了泛型,是否可以应用到类中呢?

答案是肯定的。

  1. class DataManager<T> { 
  2.   constructor(private data: <T>) {} 
  3.   getItem(index: number): <T> {    return this.data[index
  4.   }}const data = new DataManager([1]) 
  5. // const data = new DataManager<number>([1])  //直观的写法,和上面等价 
  6. data.getItem(0) 

看起来好像已经很灵活了,但是还有一个问题,没有规矩不成方圆,函数编写者允许调用者具有传参灵活度,但是需要符合函数内部的一些逻辑,也就是说之前函数 return this.data[index],但是现在函数逻辑里面,返回的是 this.data[index].name,也就是函数调用者可以传 T 类型进来,但是每一项必须要有 name 属性,这又该当如何?

那么我们可以再定义一个接口,让 T 继承接口,这样既能保持灵活度,又能符合函数逻辑。

  1. interface Item { 
  2.     name: string 
  3. }class DataManager<T extends Item> { 
  4.     constructor(private data: T[]) {} 
  5.     getItem(index: number): number { 
  6.         return this.data[index].name 
  7.     }}const data = new DataManager([ 
  8.     name'dell' 
  9. ]) 

讲到这里,泛型差不多结束了,但是还有一个疑问,上面number | string 这种联合类型想用泛型来约束,该怎么写呢,也就是 T 只能是 string 或者 number。

  1. class DataManager<T extends number | string> { 
  2.     constructor(private data: T[]) {} 
  3.     getItem(index: number): T { 
  4.         return this.data[index
  5.     }} 

命名空间

讲到这里,我们之前已经新建了很多的 demo 文件,不知道你有没有发现这样一个奇怪的现象。

demo.ts

  1. let a = 123// dosomething 

demo1.ts

  1. let a = '123' 

当我们在 demo1.ts 文件中再去定义 a 这个变量的时候,a 会标红,告诉我们 a 已经被声明了 number 类型,这是为什么呢?

我们明明在 demo1.ts 文件中没有定义过 a,再仔细看下提示,它告诉我们已经在 demo.ts 中定义过了。对 JS 很熟练的伙伴一定知道了,应该是模块化的问题。

没错,TS 跟 JS 一样,一个文件中不带有顶级的 import 或者 export 声明,它的内容是全局可见的,换句话说,如果我们文件中带有 import 或者 export,则是一个模块化。

  1. export const let a = '123' 

这样就没有问题了,我们再看下下面这段代码:

  1. class A { 
  2.   // do something 
  3. class B { 
  4.   // do something 
  5. class C { 
  6.   // do something 
  7. class D { 
  8.   constructor() { 
  9.     new A() 
  10.     new B() 
  11.     new C() 
  12.   } 

代码中,我定义了四个类,上面提到,如果我把 D 这个类通过 export 导出,这样其他文件中就可以继续使用 A 或者其他几个类名了,但是我现在有个需求是这样的,我不想把 A、B、C 三个类暴露出去,而且在外面能不能通过想通过对象的方式去调用 D 这个类。namespace 登场,看下代码:

  1. namespace Total{ 
  2.   class A { 
  3.     // do something 
  4.   } 
  5.   class B { 
  6.     // do something 
  7.   } 
  8.   class C { 
  9.     // do something 
  10.   } 
  11.   export class D { 
  12.     constructor() { 
  13.       new A() 
  14.       new B() 
  15.       new C() 
  16.     } 
  17.   } 
  18. Total.D 

这样写就可以了,通过 namespace 就只能调用到 D。如果还想调用其他类,只需要在前面去 export 这个类就好了。

namespace 在实际开发中,我们一般用在写一些 .d.ts 文件。也就是 JS 解释文件。

命名空间本质上是一个对象,它的作用就是将一系列相关的全局变量变成一个对象的属性,再看下上面的代码编译成 JS 是怎么样的。

  1. var Total; 
  2. (function (Total) { 
  3.     var A = /** @class */ (function () { 
  4.         function A() { 
  5.         }        return A; 
  6.     }());    var B = /** @class */ (function () { 
  7.         function B() { 
  8.         }        return B; 
  9.     }());    var C = /** @class */ (function () { 
  10.         function C() { 
  11.         }        return C; 
  12.     }());    var D = /** @class */ (function () { 
  13.         function D() { 
  14.             new A(); 
  15.             new B(); 
  16.             new C(); 
  17.         }        return D; 
  18.     }());    Total.D = D;})(Total || (Total = {}));Total.D; 

从上面可以看出,通过一个立即执行函数并且传了一个变量进去,然后把导出的方法挂载在变量上,这样就可以在外面通过对象属性的方式调用类。

最后再补充下 declare,它的作用是,为第三方 JS 库编写声明文件,这样才可以获得对应的代码补全和接口提示:

  1. //常用的声明类型   
  2. declare function 声明全局方法 
  3. declare class 声明全局类 
  4. declare enum 声明全局枚举类型 
  5. declare global 扩展全局变量 
  6. declare module 扩展模块 

也可以使用 declare 做模块补充。下面摘自官方的一个示例:

  1. // observable.ts 
  2. export class Observable<T> {    // ... implementation left as an exercise for the reader ... 
  3. }// map.tsimport { Observable } from "./observable"
  4. declare module "./observable" { 
  5.     interface Observable<T> {        map<U>(f: (x: T) => U): Observable<U>;    }}Observable.prototype.map = function (f) { 
  6.     // ... another exercise for the reader 
  7. }// consumer.tsimport { Observable } from "./observable"
  8. import "./map"
  9. let o: Observable<number>; 
  10. o.map(x => x.toFixed()); 

代码的意思在 map.js 中定制一个文件,补充你想要的类型 map 方法并实现函数挂载在 Observable 原型上,然后在 consumer.ts 就可以使用 Observable 类型里面的 map。

TypeScript 高级语法

类的装饰器

装饰器我们在 JS 就已经接触比较久了,并且在我的另一篇 Chat《写给前端同学容易理解并掌握的设计模式》中也详细讲解了装饰器模式,对设计模式感兴趣的同学,欢迎订阅。装饰器本质上就是一个函数。@description 这种语法其实就是一个语法糖。TS 和 JS 装饰器使用大同小异,先看一个简单的例子:

  1. function Decorator(constructor: any) { 
  2.     console.log('decorator'
  3. }@Decorator 
  4. class Demo{} 
  5. const text = new Test() 

当我们觉得完美的时候,编辑器给了我们一个标红:

 

TypeScript 从零到一,2020 开发必备

其实装饰器是一个实验性质的语法,所以不能直接使用,需要打开实验支持,修改 tsconfig 的以下两个选项:

  1. "experimentalDecorators"true
  2. "emitDecoratorMetadata"true

修改完配置之后,就发现终端正确输出了。

但是这里我还要再抛出一个问题,装饰器的运行时机是什么时候呢,是在类实例化的时候吗?

其实装饰器在类创建的时候就已经运行装饰器了,可以自行注释掉实例化语句,再运行,看控制台是否有 log。

类的装饰器修饰函数接受的参数是类的构造函数,我们可以改一下 Decorator 来验证一下:

  1. function Decorator(constructor: any) { 
  2.     constructor.prototype.getResult = () => { 
  3.         console.log('constructor'
  4.     }}@Decorator 
  5. class Demo{} 
  6. const text = new Test() 
  7. text.getResult() 

控制台正确打印出 constructor 就可以证明接收的参数确实是类的构造函数。上面的代码中我们只在类中使用了一个装饰器,但其实可以给一个类使用多个装饰器,写法如下:

  1. @Decorator 
  2. @Decorator1 
  3. class Demo{} 

多个装饰器执行顺序为先下后上。

上面的装饰器写法,我们把整个函数都给了类做装饰,但是实际情况是,我函数有一些逻辑,是不给类装饰使用的,那么我们写成一个工厂模式去给类装饰:

  1. function Decorator() { 
  2.     // do something 
  3.     return function (constructor: any) { 
  4.         console.log('descorator'
  5.     }}@Decorator()class Test() 

通过这样,我们可以传一些参数进去,然后函数内部去控制装饰器的装饰。

不知道你有没有发现,我们在验证装饰器参数的时候,当我们通过类的实例去调用我们挂载在装饰器原型的方法的时候,虽然没有报错,但是编辑器没有给我们提示,这是很不符合我们预期的。上面那种装饰器写法很简单,但很直观。

但在 TS 中我们往往是像下面这种方式使用的,而且也能解决上面提到的那个问题:

  1. function Decorator() { 
  2.     return function <T extends new (...args: any[]) => any>(constructor: T) { 
  3.         return class extends constructor{ 
  4.             name = 'bbb' 
  5.             getName        }    }} const Test = Decorator()( class { 
  6.     name: string 
  7.     constructor(name: string) { 
  8.         console.log(this.name,'1'
  9.         this.name = name 
  10.         console.log(this.name,'2'
  11.     }})const test = new Test('aaa'
  12. console.log(test.getName()) 

我们把之前的代码大变样,看起来似乎高大上了许多,但是理解起来也挺有难度的。别急,让我来一一进行解释。

  1. <T extends new (...args: any[]) => any

这个是一个泛型,T 继承了一个构造函数也可以说是继承了一个类,构造函数参数是一个展开运算符,表示接收多个参数。

这样泛型 T 就可以用来定义 constructor。而 Decorator 函数,跟上面一样,我们写成函数柯里化形式,并且把类作为参数传递进去,摒弃了之前的语法糖,这样我们在调用装饰在类上的方法的时候编辑器就能给我们提示。

方法装饰器

上一节,分享完了类的装饰器,大家肯定对装饰器意犹未尽,这一小节,再分享下给类的方法装饰,先上个代码,来看下:

  1. function getNameDecorator( 
  2.   target: any
  3.   key: string, 
  4.   descriptor: PropertyDescriptor 
  5. ) { 
  6.   console.log(target); 
  7. } class Test { 
  8.      name: string 
  9.      constructor(name: string) { 
  10.          this.name = name 
  11.      }     @getNameDecorator 
  12.      getName() {         return this.name 
  13.      } }const test - new Test('aaa'
  14. console.log(test.getName()) 

这就实现了给类的方法进行装饰,当我们给类的普通方法进行装饰的时候,装饰器函数中接收的参数 target 对应的是类的 prototype,key 是装饰的普通方法的名字。

注意,我上面说的是普通方法。和类的装饰器一样,方法装饰器的执行时机同样是当方法被定义的时候。

刚才我已经强调了普通方法,接下来我就要说静态方法了。

  1. class Test { 
  2.     name: string 
  3.     constructor(name: string) { 
  4.         this.name = name 
  5.     }    @getNameDecorator 
  6.     static getName() { 
  7.         return this.name 
  8.     }} 

静态方法的装饰器函数中,第一个参数 target 对应的是类的构造函数。

类的方法装饰器函数中,我们还有一个参数没有讲,那就是 descriptor。

不知道你有没有发现,这个函数接收三个参数,而且第三个参数还是 descriptor,有点像 Object.defineProperty 这个 API,当我们在函数中调用 descriptor 的时候,编辑器会给我们提示。

这几个属性和 Object.defineProperty 中的 descriptor 可设置属性一样,没错,功能也是一样的.比如,我们不想在外部,getName 方法被重写,那么我们可以这样:

  1. function getNameDecorator( 
  2.   target: any
  3.   key: string, 
  4.   descriptor: PropertyDescriptor 
  5. ) { 
  6.   console.log(target); 
  7.   descriptor.writable = false 

当你试图这样去修改它的时候,运行编译后文件将会报错:

  1. const test = new Test('aaa'
  2. console.log(test.getName()) 
  3. test.getName = () => { 
  4.     return 'aaa' 

这是运行结果:

 

TypeScript 从零到一,2020 开发必备

访问器装饰器

在 ES6 的 class 中新增访问器,通过 get 和 set 方法访问属性,如果上面的知识点你都消化了,那么访问器装饰器的用法也是如出一辙。

  1. function visitDecorator( 
  2.     target: any
  3.     key: string, 
  4.     descriptor: PropertyDescriptor 
  5. ){} 
  6. class Test { 
  7.     provate _name: string 
  8.     constructor(name: string) { 
  9.         this._name = name 
  10.     }    get name() { 
  11.         return this._name 
  12.     }    @visitDecorator 
  13.     set name() { 
  14.         this._name = name 
  15.     }} 

访问器装饰器的用法跟类的普通方法装饰器用法差不多,这里就不展开来讲了。同样地,在类中,我们也可以给属性添加装饰器,参数添加装饰器。

装饰器业务场景使用

之前我们花了比较长的篇幅来介绍装饰器,这一小节,将跟大家分享下实际业务场景中,装饰器的使用。首先来看这样一段代码:

  1. const uerInfo: any = undefined 
  2. class Test {    getName() {        return userInfo.name 
  3.     }    getAge() {        return userInfo.name 
  4.     }}const test = new Test() 
  5. test.getName() 

这段代码不用运行,我们都能知道,会报错,因为 userInfo 没有 name 属性。因此如果我们想要不报错,就会写成这样:

  1. class Test { 
  2.     getName() {        try { 
  3.             return userInfo.name 
  4.         } catch (e) { 
  5.             console.log('userInfo.name 不存在'
  6.         }    }    getAge() {        try { 
  7.             return userInfo.age 
  8.         } catch (e) { 
  9.             console.log('userInfo.age 不存在'
  10.         }    }} 

把类改成这样,似乎就没有问题了,为什么说似乎呢?

那是因为运行虽然没有问题,但是如果我们还有很多类似于这样的方法,我们是否要重复处理错误呢?能否用到之前讲的装饰器来处理错误:

  1. const userInfo: any = undefined 
  2. function catchError(    target: any
  3.     key: string, 
  4.     descriptor: PropertyDescriptor){    const fn = descriptor.value 
  5.     descriptor.value = function() { 
  6.         try { 
  7.             fn()        } catch (e) { 
  8.             console.log('userinfo 出问题啦'
  9.         }    }}class Test { 
  10.     @catchError 
  11.     getName() {        return userInfo.name 
  12.     }    @catchError 
  13.     getAge() {        return userInfo.age 
  14.     }} 

这样我们就把捕获异常的逻辑提取出来了,通过装饰器来复用。

但是和我们之前写的还有点差异,就是报错信息都一样,我们不知道具体是哪个函数报的错,也就是说,我们希望装饰器函数可以接收一个参数,来完善报错信息,这样的话,我们就可以用到讲过的,把装饰器包装成一个工厂函数,代码如下:

  1. function catchError(msg: string) { 
  2.     return function ( 
  3.         target: any
  4.         key: string, 
  5.         descriptor: PropertyDescriptor 
  6.     ){ 
  7.         const fn = descriptor.value 
  8.         descriptor.value = function() { 
  9.             try { 
  10.                 fn()            } catch (e) { 
  11.                 console.log(`userinfo.${msg} 出问题啦`) 
  12.             }        }    }}class Test { 
  13.     @catchError('name'
  14.     getName() {        return userInfo.name 
  15.     }    @catchError('age)' 
  16.     getAge() {        return userInfo.age 
  17.     }} 

这样我们的代码就能满足我们的需求了,后面我们再添加其他函数函数,也可以用装饰器对其进行装饰。

项目中应用 TypeScript

脚手架搭建一个 TypeScript

现在的开发越来越专业,一般我们初始化一个项目,如果不用脚手架进行开发的话,需要自己去配置一大堆东西,比如 package.json、.gitignore,还有一些构建工具,像 webpack 等以及他们的配置。

而当我们去使用 TypeScript 编写一个项目的时候,还需要配置 TypeScript 的编译配置文件 tsconfig 以及 tslint.json 文件。

如果我们只是想做一个小项目或者只想学习这块的开发,那前期的磨刀准备工作将让很多人望而却步,一头雾水。因此,一个脚手架工具就可以帮我们把刀磨好,而且磨的铮鲜亮丽的,这个工具就是 TypeScript Library Starter。让我们一起来了解下。

查看它的官网,我们知道这是一个以 TypeScript 为基础的开源脚手架工具,帮助我们快速开始一个 TypeScript 项目,使用方法如下:

  1. git clone https://github.com/alexjoverm/typescript-library-starter.git ts-project 
  2. cd ts-projectnpm install 

这几行命令的意思是,把代码拉下来然后给项目重命名。进入到项目,通过 npm install 去给项目安装依赖,然后我们来看下我们的文件目录:

 

TypeScript 从零到一,2020 开发必备

├── package.json  // 项目配置文件 

  1. ├── rollup.config.ts // rollup 配置文件 
  2. ├── src // 源码目录 
  3. ├── test // 测试目录 
  4. ├── tools // 发布到 GitHup pages 以及 发布到 npm 的一些配置脚本工具 
  5. ├── tsconfig.json // TypeScript 编译配置文件 
  6. └── tslint.json // TypeScript lint 文件 

TypeScript library starter 创建的项目确实集成了很多优秀的开源工具,包括打包、单元测试、格式化代码等,有兴趣的同学可以自行深入研究下。

还有需要介绍的是,TypeScript library starter 在 package.json 中帮我们配置了一套完整的前端工作流:

  • npm run lint:使用 TSLint 工具检查 src 和 test 目录下 TypeScript 代码的可读性、可维护性和功能性错误。
  • npm start:观察者模式运行 rollup 工具打包代码。
  • npm test:运行 Jest 工具跑单元测试。
  • npm run commit:运行 commitizen 工具提交格式化的 git commit 注释。
  • npm run build:运行 rollup 编译打包 TypeScript 代码,并运行 typedoc 工具生成文档。

其他一些命令在我们日常开发中使用不是非常多,有需要的同学可以再自行去了解。

TypeScript 实战

现在我们的前端项目基本都是使用框架进行开发,今天我就介绍如何使用 React + TypeScript 进行 React 项目开发。当然这里我们还是会使用 React 提供的脚手架迅速搭建项目框架,为了避免你本地之前的脚手架版本影响 TypeScript 的开发,建议先执行:

  1. npm uninstall create-react-app 

然后执行官方提供的 React TypeScript 生成命令:

  1. npx create-react-app react-project --template typescript --use-npm 

这个命令的意思是下载最新脚手架(如果当前环境没有这个脚手架的话),然后通过 create-react-app 脚手架去生成以 typescript 为开发模板的项目,项目名字叫 react-project,并通过 npm 去安装依赖,如果没有 --use-npm 则会默认是使用 Yarn。

项目搭建完成之后,我们把文件整理下,删除一些我们不用的文件,同时把相关引用也删除,最终文件目录如下:

 

TypeScript 从零到一,2020 开发必备

当我们使用 TS 去写 React 的时候, jsx 就变成了 tsx。在 APP.tsx 文件中:

 

  1. const App: React.FC = () => { 
  2.     return <div className="App"></div> 

通过 React.FC 给函数定义了一个 React.FC 的函数类型,这是 React 中定义的函数类型。

前端 UI 开发,现在市面上也有很多封装好的框架,让我们可以快速搭建一个页面,这里我们选用 ant-design,这个框架也是使用 TypeScript 进行开发的,所以我们使用它进行开发的时候,会有很多类型可以供我们使用,因此使用它去巩固我们刚学习的 TypeScript 知识点会有更多的好处。

首先让我们来安装下这个组件库:

  1. npm install antd --save 

安装好之后,再 index.tsx 中引入 CSS 样式:

  1. import 'antd/dist/antd.css' 

接下来我们去写个登录页面,首页新建一个 login.css:

  1. .login-page { 
  2.   width: 300px; 
  3.   padding: 20px; 
  4.   margin: 100px auto; 
  5.   border: 1px solid #ccc; 

然后我们去 antd-design 官网,把登录组件代码复制到我们的 App.ts 中:

 

  1. import React from "react"
  2. // import ReactDOM from 'react-dom' 
  3. import "./login.css"
  4. // function App() { 
  5. //   return <div className="login-page">Hello world</div>; 
  6. // } 
  7. // export default App; 
  8. import { Form, Input, Button, Checkbox } from "antd"
  9. // import { Store } from "antd/lib/form/interface"
  10. import { ValidateErrorEntity, Store } from "rc-field-form/lib/interface"
  11. const layout = {  labelCol: {    span: 8, 
  12.   },  wrapperCol: {    span: 16, 
  13.   },};const tailLayout = {  wrapperCol: {    offset: 8, 
  14.     span: 16, 
  15.   },};const App = () => { 
  16.   const onFinish = (values: Store) => { 
  17.     console.log("Success:"values); 
  18.   };  // const onFinishFailed = (errorInfo: Store) => { 
  19.   const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 
  20.     console.log("Failed:", errorInfo); 
  21.   };  return ( 
  22.     <div className="login-page"
  23.       <Form        {...layout}        name="basic" 
  24.         initialValues={{          remember: true
  25.         }}        onFinish={onFinish}        onFinishFailed={onFinishFailed}      >        <Form.Item          label="Username" 
  26.           name="username" 
  27.           rules={[            {              required: true
  28.               message: "Please input your username!"
  29.             },          ]}        >          <Input /> 
  30.         </Form.Item> 
  31.         <Form.Item 
  32.           label="Password" 
  33.           name="password" 
  34.           rules={[ 
  35.             { 
  36.               required: true
  37.               message: "Please input your password!"
  38.             }, 
  39.           ]} 
  40.         > 
  41.           <Input.Password /> 
  42.         </Form.Item> 
  43.         <Form.Item {...tailLayout} name="remember" valuePropName="checked"
  44.           <Checkbox>Remember me</Checkbox> 
  45.         </Form.Item> 
  46.         <Form.Item {...tailLayout}> 
  47.           <Button type="primary" htmlType="submit"
  48.             Submit 
  49.           </Button> 
  50.         </Form.Item> 
  51.       </Form> 
  52.     </div> 
  53.   ); 
  54. }; 
  55. // ReactDOM.render(<Demo />, mountNode); 
  56. export default App; 

其中,onFinish 函数的 values 编辑器给我们报隐患提示,我们也无法确定 value 的类型,但是又不能填写 any。因此,我们可以去找下 Form 中定义的类型。mac 用户把鼠标放在 import 中的 From 标签上( windows 用户按住 cmd),进入到源代码中去,然后一直去查找我们的方法的定义,首先我们进入到了:

 

TypeScript 从零到一,2020 开发必备

然后 InternalForm 继承了 InternalForm,我们再继续去寻找,最后找到了源头:

 

TypeScript 从零到一,2020 开发必备

同理我们也可以找到 onFinishFailed:

 

TypeScript 从零到一,2020 开发必备

最后在文件中引入这两个类型即可。

经过上面的测试之后,我们的项目基本上就算已经搭建好了,接下来就可以继续充实相关的页面了。

这里再把文件整理下,把不需要的删除,src 目录下新建一个 pages 的目录,然后我们的页面组件都放在这里,把 login 的代码也在这个文件夹下新建一个文件存放,然后我们再修改下 App.ts:

 

  1. import { Route, HashRouter, Switch } from "react-router-dom"
  2. import React from "react"
  3. import LoginPage from "./pages/login"
  4. import Home from "./pages/home"
  5. function App() { 
  6.   return ( 
  7.     <div> 
  8.       <HashRouter> 
  9.         <Switch> 
  10.         <Route path="/" exact component={Home}></Route> 
  11.           <Route path="/login" exact component={LoginPage}></Route> 
  12.         </Switch> 
  13.       </HashRouter> 
  14.     </div> 
  15.   );}export default App; 

由于 react-router-dom 是 JS 编写的文件,因此需要再安装一个类型定义文件:

  1. npm install @types/react-router-dom -D 

因为本篇篇幅的原因,项目后面的深入就不继续了,大家在闲暇之余可以继续享受用 TS 编写代码带来的快感。

责任编辑:未丽燕 来源: 今日头条
相关推荐

2021-10-28 07:10:21

rollupPlugin插件编写

2021-07-12 07:33:31

Nacos微服务管理

2021-01-27 07:24:38

TypeScript工具Java

2022-02-13 23:00:48

前端微前端qiankun

2023-04-06 08:01:30

RustMutex

2021-06-30 07:51:09

新项目领域建模

2024-06-12 09:06:48

2017-04-10 14:23:01

typescriptjavascriptwebpack

2022-01-27 13:02:46

前端爬虫工具

2023-05-24 08:00:00

2021-08-07 21:51:17

服务器网站部署

2023-01-12 22:00:48

2013-12-18 13:30:19

Linux运维Linux学习Linux入门

2019-06-10 15:00:27

node命令行前端

2017-03-28 09:26:01

数据必备技能

2021-08-15 22:52:30

前端H5拼图

2024-04-26 08:17:09

GoGoogle项目

2010-02-06 15:31:18

ibmdwAndroid

2009-07-22 14:55:16

ibmdwAndroid

2012-04-26 10:48:01

iOS开发互动广告
点赞
收藏

51CTO技术栈公众号