一切福田,不離方寸,從心而覓,感無不通。

最全的TypeScript学习指南

前言

Hello 大家好 我是鲨鱼哥 这次给大家带来的是我曾经非常嫌弃 如今却爱不释手TS 技术 哈哈 大家看往期文章可能已经发现鲨鱼哥之前主要是 Vue 技术栈的 然后因为 Vue2 和 TS 的结合总感觉不是很丝滑 所以我果断就在技术选型的时候去掉了 TS(其实我是觉得用起来很烦 和我之前最讨厌的 eslint 一样 各种报错让人不爽)但是 鲨鱼哥今年换了新公司 开启了全新的 react hook+ts 这一套组合拳 然后在重新认真学习并在项目里用上了 ts 之后 确实真香 哈哈 最直观的感受就是可以帮我们规避很多类型错误 更友好的提示 甚至有些方法我们根据定义的类型大概就知道作用是什么了(去掉了写注释的麻烦)况且如今大火的 Vue3 也是 TS 重构的 然后 react 和 ts 的结合就更不必说了 所以还没有开始 ts 的同学就从现在开始跟着鲨鱼哥一起来学习吧 最后欢迎大家点击 链接 加入到鲨鱼哥的前端群 内推 讨论技术 摸鱼 求助 皆可

整理不易 如果觉得本文有帮助 记得点赞三连哦 十分感谢!


1 ts 安装和编译

  • 第一步 新建一个空文件夹用来学习 ts
  • 第二步 全局安装 ts 和 ts-node

 

  • 第三步 生成 tsconfig.js 配置文件

 

我们就先按照自动生成的 tsconfig 配置项去使用 里面的配置咱们可以先不去管他 后续熟练了再去配置

  • 第四步 在项目下新建一个index.ts 直接写入

 

  • 第五步 编译 ts 为 js 在控制台(终端)输入命令

 

神奇的事情发生了 项目下出现了一个同名的 index.js 文件 至此我们已经可以把 ts 文件编译成 js 文件了

不过到这里聪明的小伙伴就会发现了 我们全局安装的 ts-node 有什么作用呢 其实这个包是帮助我们在不需要编译成 js 的前提下就可以直接执行 ts 代码 比如 我们在控制台输入

 

可以看到我们打印的hello已经输出了

那可能 还有的小伙伴会发现 我们每次改动都要手动去执行编译 这样很麻烦 其实我们可以加一个参数来实现每次文件变动 ts 帮我们自动编译成 js 的效果

 

好了 环境安装完毕了 接下来出发去学习 ts 核心吧

2 TS 类型

2.1 布尔类型(boolean)

 

2.2 Number 类型

 

2.3 String 类型

 

2.4 Enum 类型

使用枚举我们可以很好的描述一些特定的业务场景,比如一年中的春、夏、秋、冬,还有每周的周一到周天,还有各种颜色,以及可以用它来描述一些状态信息,比如错误码等

 

2.5 数组类型(array)

 

2.6 元组类型(tuple)

在 TypeScript 的基础类型中,元组( Tuple )表示一个已知数量类型的数组 其实可以理解为他是一种特殊的数组

 

2.7 Symbol

我们在使用 Symbol 的时候,必须添加 es6 的编译辅助库 需要在 tsconfig.json 的 libs 字段加上ES2015 Symbol 的值是唯一不变的

 

2.8 任意类型(any)

任何类型都可以被归为 any 类型 这让 any 类型成为了类型系统的 顶级类型 (也被称作 全局超级类型) TypeScript 允许我们对 any 类型的值执行任何操作 而无需事先执行任何形式的检查

一般使用场景: 第三方库没有提供类型文件时可以使用 any 类型转换遇到困难或者数据结构太复杂难以定义 不过不要太依赖 any 否则就失去了 ts 的意义了

 

2.9 null 和 undefined

undefinednull 两者有各自的类型分别为 undefinednull

 

2.10 Unknown 类型

unknownany 的主要区别是 unknown 类型会更加严格 在对 unknown 类型的值执行大多数操作之前 我们必须进行某种形式的检查 而在对 any 类型的值执行操作之前 我们不必进行任何检查 所有类型都可以被归为 unknownunknown类型只能被赋值给 any 类型和 unknown 类型本身 而 any 啥都能分配和被分配

 

2.11 void 类型

void 表示没有任何类型 当一个函数没有返回值时 TS 会认为它的返回值是 void 类型。

 

2.12 never 类型

never 一般表示用户无法达到的类型 例如never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

 

思考:never 和 void 的区别 void 可以被赋值为 null 和 undefined 的类型。 never 则是一个不包含值的类型。 拥有 void 返回值类型的函数能正常运行。拥有 never 返回值类型的函数无法正常返回,无法终止,或会抛出异常。

2.13 BigInt 大数类型

使用 BigInt 可以安全地存储和操作大整数 我们在使用 BigInt 的时候 必须添加 ESNext 的编译辅助库 需要在 tsconfig.json 的 libs 字段加上ESNext 要使用1n需要 "target": "ESNext" numberBigInt 类型不一样 不兼容

 

2.14 object, Object 和 {} 类型

object 类型用于表示非原始类型

 

大 Object 代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 nullundefined 不可以)

{} 空对象类型和大 Object 一样 也是表示原始类型和非原始类型的集合

 

 

2.15 类型推论

指编程语言中能够自动推导出值的类型的能力 它是一些强静态类型语言中出现的特性 定义时未赋值就会推论成 any 类型 如果定义的时候就赋值就能利用到类型推论

 

2.16 联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种 未赋值时联合类型上只能访问两个类型共有的属性和方法

 

2.17 类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。其实就是你需要手动告诉 ts 就按照你断言的那个类型通过编译(这一招很关键 有时候可以帮助你解决很多编译报错)

类型断言有两种形式:

 

以上两种方式虽然没有任何区别,但是尖括号格式会与 react 中 JSX 产生语法冲突,因此我们更推荐使用 as 语法。

非空断言 在上下文中当类型检查器无法断定类型时 一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型

 

2.18 字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。 目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:

 

2.19 类型别名

类型别名用来给一个类型起个新名字

 

2.20 交叉类型

交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

 

2.21 类型保护

类型保护就是一些表达式,他们在编译的时候就能通过类型信息确保某个作用域内变量的类型 其主要思想是尝试检测属性、方法或原型,以确定如何处理值

typeof 类型保护

 

in 关键字

 

instanceof 类型保护

 

自定义类型保护

通过 type is xxx这样的类型谓词来进行类型保护

例如下面的例子 value is object就会认为如果函数返回 true 那么定义的 value 就是 object 类型

 

3 函数

3.1 函数的定义

可以指定参数的类型和返回值的类型

 

3.2 函数表达式

定义函数类型

 

3.3 可选参数

在 TS 中函数的形参和实参必须一样,不一样就要配置可选参数,而且必须是最后一个参数

 

3.4 默认参数

 

3.5 剩余参数

 

3.6 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 在 TypeScript 中,表现为给同一个函数提供多个函数类型定义

 

注意:函数重载真正执行的是同名函数最后定义的函数体 在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法 只能定义类型

4 类

4.1 类的定义

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类

 

当然 如果我们图省事 我们也可以把属性定义直接写到构造函数的参数里面去(不过一般不建议这样写 因为会让代码增加阅读难度)

 

注意:当我们定义一个类的时候,会得到 2 个类型 一个是构造函数类型的函数类型(当做普通构造函数的类型) 另一个是类的实例类型(代表实例)

具体看例子

 

4.2 存取器

在 TypeScript 中,我们可以通过存取器来改变一个类中属性的读取和赋值行为

 

其实我们可以看看翻译成 es5 的代码 原理很简单 就是使用了 Object.defineProperty 在类的原型上面拦截了属性对应的 get 和 set 方法

 

4.3 readonly 只读属性

readonly 修饰的变量只能在构造函数中初始化 TypeScript 的类型系统同样也允许将 interface、type、 class 上的属性标识为 readonly readonly 实际上只是在编译阶段进行代码检查。

 

4.4 继承

子类继承父类后子类的实例就拥有了父类中的属性和方法,可以增强代码的可复用性

将子类公用的方法抽象出来放在父类中,自己的特殊逻辑放在子类中重写父类的逻辑

super 可以调用父类上的方法和属性

在 TypeScript 中,我们可以通过 extends 关键字来实现继承

 

4.5 类里面的修饰符

public 类里面 子类 其它任何地方外边都可以访问 protected 类里面 子类 都可以访问,其它任何地方不能访问 private 类里面可以访问,子类和其它任何地方都不可以访问

 

4.6 静态属性 静态方法

类的静态属性和方法是直接定义在类本身上面的 所以也只能通过直接调用类的方法和属性来访问

 

4.7 抽象类和抽象方法

抽象类,无法被实例化,只能被继承并且无法创建抽象类的实例 子类可以对抽象类进行不同的实现

抽象方法只能出现在抽象类中并且抽象方法不能在抽象类中被具体实现,只能在抽象类的子类中实现(必须要实现)

使用场景: 我们一般用抽象类和抽象方法抽离出事物的共性 以后所有继承的子类必须按照规范去实现自己的具体逻辑 这样可以增加代码的可维护性和复用性

使用 abstract 关键字来定义抽象类和抽象方法

 

思考 1:重写(override)和重载(overload)的区别

重写是指子类重写继承自父类中的方法 重载是指为同一个函数提供多个类型定义

 

思考 2:什么是多态

在父类中定义一个方法,在子类中有多个实现,在程序运行的时候,根据不同的对象执行不同的操作,实现运行时的绑定。

 

5 接口

接口既可以在面向对象编程中表示为行为的抽象,也可以用来描述对象的形状

我们用 interface 关键字来定义接口 在接口中可以用分号或者逗号分割每一项,也可以什么都不加

5.1 对象的形状

 

5.2 行为的抽象

接口可以把一些类中共有的属性和方法抽象出来,可以用来约束实现此接口的类

一个类可以实现多个接口,一个接口也可以被多个类实现

我们用 implements关键字来代表 实现

 

5.3 定义任意属性

如果我们在定义接口的时候无法预先知道有哪些属性的时候,可以使用 [propName:string]:any,propName 名字是任意的

 

这个接口表示 必须要有 id 和 name 这两个字段 然后还可以新加其余的未知字段

5.4 接口的继承

我们除了类可以继承 接口也可以继承 同样的使用 extends关键字

 

5.5 函数类型接口

可以用接口来定义函数类型

 

5.6 构造函数的类型接口

使用特殊的 new()关键字来描述类的构造函数类型

 

其实这样的用法一般出现在 当我们需要把一个类作为参数的时候 我们需要对传入的类的构造函数类型进行约束 所以需要使用 new 关键字代表是类的构造函数类型 用以和普通函数进行区分


思考:接口和类型别名的区别 这个题目是经典的 ts 面试题

实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。

1.基础数据类型 与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组

 

2.重复定义

接口可以定义多次 会被自动合并为单个接口 类型别名不可以重复定义

 

3.扩展 接口可以扩展类型别名,同理,类型别名也可以扩展接口。但是两者实现扩展的方式不同

接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

 

4.实现 这里有一个特殊情况 类无法实现定义了联合类型的类型别名

 

6 泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性

为了更好的了解泛型的作用 我们可以看下面的一个例子

 

上述这段代码用来生成一个长度为 length 值为 value 的数组 但是我们其实可以发现一个问题 不管我们传入什么类型的 value 返回值的数组永远是 any 类型 如果我们想要的效果是 我们预先不知道会传入什么类型 但是我们希望不管我们传入什么类型 我们的返回的数组的指里面的类型应该和参数保持一致 那么这时候 泛型就登场了

使用泛型改造

 

我们可以使用<>的写法 然后再面传入一个变量 T 用来表示后续函数需要用到的类型 当我们真正去调用函数的时候再传入 T 的类型就可以解决很多预先无法确定类型相关的问题

6.1 多个类型参数

如果我们需要有多个未知的类型占位 那么我们可以定义任何的字母来表示不同的类型参数

 

6.2 泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

 

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束

 

注意:我们在泛型里面使用extends关键字代表的是泛型约束 需要和类的继承区分开

6.3 泛型接口

定义接口的时候也可以指定泛型

 

我们定义了接口传入的类型 T 之后返回的对象数组里面 T 就是当时传入的参数类型

6.4 泛型类

 

上诉例子我们实现了一个在数组里面添加数字并且获取最大值的泛型类

6.5 泛型类型别名

 

6.6 泛型参数的默认类型

我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

 

7 实用技巧

7.1 typeof 关键词

typeof 关键词除了做类型保护 还可以从实现推出类型,

 

上面的例子就是使用 typeof 获取一个变量的类型

7.2 keyof 关键词

keyof 可以用来取得一个对象接口的所有 key 值

 

7.3 索引访问操作符

使用 [] 操作符可以进行索引访问

 

7.4 映射类型 in

在定义的时候用 in 操作符去批量定义类型中的属性

 

7.5 infer 关键字

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

 

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

7.6 内置工具类型
  1. Exclude<T,U> 从 T 可分配给的类型中排除 U

 

  1. Extract<T,U> 从 T 可分配给的类型中提取 U

 

  1. NonNullable 从 T 中排除 nullundefined

 

  1. ReturnType infer 最早出现在此 PR 中,表示在 extends 条件语句中待推断的类型变量

 

可见 该工具类型主要是获取函数类型的返回类型

  1. Parameters 该工具类型主要是获取函数类型的参数类型

 

  1. Partial Partial 可以将传入的属性由非可选变为可选

 

  1. Required Required 可以将传入的属性中的可选项变为必选项,这里用了 -? 修饰符来实现。

 

  1. Readonly Readonly 通过为传入的属性每一项都加上 readonly 修饰符来实现。

 

  1. Pick<T,K> Pick 能够帮助我们从传入的属性中摘取某些返回

 

  1. Record<K,T> 构造一个类型,该类型具有一组属性 K,每个属性的类型为 T。可用于将一个类型的属性映射为另一个类型。Record 后面的泛型就是对象键和值的类型。

简单理解:K 对应对应的 key,T 对应对象的 value,返回的就是一个声明好的对象 但是 K 对应的泛型约束是keyof any 也就意味着只能传入 string|number|symbol

 

  1. Omit<K,T> 基于已经声明的类型进行属性剔除获得新类型

 

8 TypeScript 装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为

常见的装饰器有类装饰器、属性装饰器、方法装饰器和参数装饰器

装饰器的写法分为普通装饰器和装饰器工厂

使用@装饰器的写法需要把 tsconfig.json 的 experimentalDecorators 字段设置为 true

8.1 类装饰器

类装饰器在类声明之前声明,用来监视、修改或替换类定义

 

8.2 属性装饰器

属性装饰器表达式会在运行时当作函数被调用,传入 2 个参数 第一个参数对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 第二个参数是属性的名称

 

8.3 方法装饰器

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数: target: Object – 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 propertyKey: string | symbol – 方法名 descriptor: TypePropertyDescript – 属性描述符

 

8.4 参数装饰器

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

target: Object – 被装饰的类 propertyKey: string | symbol – 方法名 parameterIndex: number – 方法中参数的索引值

 

以上代码成功运行后,控制台会输出以下结果: "The parameter in position 0 at Greeter has been decorated"

8.5 装饰器执行顺序

有多个参数装饰器时:从最后一个参数依次向前执行

方法和方法参数中参数装饰器先执行。 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行

类装饰器总是最后执行

 

9 编译

9.1 tsconfig.json 的作用
  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。
9.2 tsconfig.json 重要字段
  • files – 设置要编译的文件的名称;
  • include – 设置需要进行编译的文件,支持路径模式匹配;
  • exclude – 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions – 设置与编译流程相关的选项。
9.3 compilerOptions 选项

 

10 模块和声明文件

10.1 全局模块

在默认情况下,当你开始在一个新的 TypeScript 文件中写下代码时,它处于全局命名空间中

使用全局变量空间是危险的,因为它会与文件内的代码命名冲突。我们推荐使用下文中将要提到的文件模块

foo.ts

 

bar.ts

 

10.2 文件模块
  • 文件模块也被称为外部模块。如果在你的 TypeScript 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域
  • 模块是 TS 中外部模块的简称,侧重于代码和复用
  • 模块在其自身的作用域里执行,而不是在全局作用域里
  • 一个模块里的变量、函数、类等在外部是不可见的,除非你把它导出
  • 如果想要使用一个模块里导出的变量,则需要导入

foo.ts

 

bar.ts

 

10.3 声明文件
  • 我们可以把类型声明放在一个单独的类型声明文件中
  • 文件命名规范为*.d.ts
  • 查看类型声明文件有助于了解库的使用方式

typings\jquery.d.ts

 

10.4 第三方声明文件
  • 可以安装使用第三方的声明文件
  • @types 是一个约定的前缀,所有的第三方声明的类型库都会带有这样的前缀
  • JavaScript 中有很多内置对象,它们可以在 TypeScript 中被当做声明好了的类型
  • 内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准
  • 这些内置对象的类型声明文件,就包含在 TypeScript 核心库的类型声明文件中,具体可以查看ts 核心声明文件
10.5 查找声明文件
  • 如果是手动写的声明文件,那么需要满足以下条件之一,才能被正确的识别
  • 给 package.json 中的 types 或 typings 字段指定一个类型声明文件地址
  • 在项目根目录下,编写一个 index.d.ts 文件
  • 针对入口文件(package.json 中的 main 字段指定的入口文件),编写一个同名不同后缀的 .d.ts 文件

 

查找过程如下:

1.先找 myLib.d.ts

2.没有就再找 index.d.ts

3.还没有再找 lib/index.d.js

4.还找不到就认为没有类型声明了

小结

能看到此处的小伙伴估计是真爱粉了 哈哈 其实 ts 没有大家想象的那么难 可能刚开始接触的时候会比较抵触 或者觉得难用 但是只要坚持下去 慢慢就会发现真香定律 咱们一开始也没有必要去追求多么花哨或者高级的用法 如果在没有办法的情况下就用any 大法也不是不可以 总之首先要用起来 只有不断地基于实战练习最终才能掌握一门技术的精髓 鲨鱼哥这篇文档只是理论知识 大家一定要下去多练习 另外 这篇文档是基于目前网上多方资源和鲨鱼哥自己的思考整理出来的个人觉得比较全面的 ts 学习指南 也感谢很多的优秀博主之前出品的 ts 文章 比如 阿宝哥 俊劫 Jimmy_kiwi 等等

作者:Big shark@LX
链接:https://juejin.cn/post/7031787942691471396
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。