学习TypeScript
TypeScript
TypeScript 介绍
TypeScript 简称:TS,是 JavaScript 的超集,简单来说就是:JavaScript 有的 TypeScript 都有

TypeScript 实际上就是 JavaScript(弱类型) + Type (类型) , 即为 JS 添加类型支持,如图
1 | // 为username声明明确的变量类型 字符串类型 |
TypeScript 为什么要为 JS 添加类型支持
Javascript 类型系统存在先天缺陷,我们在写代码时经常遇到类型错误(Uncaught TypeError)
这类错误的出现导致项目开发完成上线之后,要投入大量的精力和时间去测试,找 bug,改 bug,对于开发效率是一种降低。
问题
为什么 JS 在开发时不能提前预知,预防这些错误呢?
- 因为 Javascript 属于动态类型的编程语言,动态类型最大的特点就是它只能在代码执行期间做类型的相关检查,所以往往你发现问题的时候,已经晚了。
TS 能提前预防这些错误吗?
- 可以。 TypeScript 属于静态类型的编程语言。也就是代码会先通过编译,然后运行,编译不通过的,自然暴露了我们代码中的问题。
优势
- 更早的发现错误,减少找 Bug,改 Bug,提升开发效率。
- 程序中随时出现代码提示,随时随地的安全感,增强的开发体验。
- 强大的类型系统提高代码可维护性,重构代码更容易。
- 支持最新的 ECMAScript 语法,优先体验最新的语法,让你走上前端技术最前沿。
- TS 的类型推断机制,不需要在每个地方都标注类型,降低学习负担,除此之外,Vue3 源码使用 TS 重写,Angular 默认支持 TS,React 于 TS 完美配合。
当下最主流的开发技术栈
Vue3 + TS
Reack Hooks + TS
快速上手
- 安装编译 TS 的工具包
- 编译运行 TS 代码
- 简化运行 TS 的步骤
安装编译 TS 的依赖
为什么要安装依赖?
- Nodejs/浏览器 只认识 JavaScript 代码,不识 TS 代码,需要先将 TS 代码转化为 JS 代码,才可以运行。
安装命令
1 | $ npm i -g typescript |
验证是否安装成功
1 | $ tsc -v |
tsc 为关键字
运行流程图

编译运行 ts 代码
- 创建一个 hello.ts 文件 (TS 的文件的后缀名为.ts)
- 将 TS 编译为 JS,在终端编译命令 tsc hello.ts (此时,同级目录会出现一个同名 JS 文件)
- 执行 JS 代码,在终端输入命令 node hello.js,使用node运行代码

使用 ts-node 简化运行 TS 的步骤
通过上面的描述,我们发现运行 TS 代码需要连续的执行两个命令,有些繁琐,可以使用**ts-node**进行简化
安装命令
1 | $ npm i -g ts-node |
**ts-node**包提供了 ts-node 命令,相当于前面两个命令的合并, 接下来就可以直接采用 命令执行运行 ts 文件
1 | $ ts-node hello.ts |
OK, 接下来我们就可以愉快的来学习 TypeScript 了
注意: 我们在运行过程中,可能会遇到如图的问题

这个问题是因为当前的语法中 es6 的配置没有,需要我们用一个命令来进行设置
1 | $ tsc --init # 初始化一个配置 此配置会给我们加一个es6的指向,错误就不再有了 |
解决两个 ts 文件之间的变量名冲突
问题:在非模块化环境下,TS 会把 .ts 文件中声明的变量当做全局变量
所以,当两个 .ts 文件中声明的变量名称相同,并且在 VSCode 中同时打开这两个文件时,VSCode 会提示报错
虽然,不会影响代码的运行。但看到报错会让人感觉不舒服,所以,只要让 .ts 文件中的代 码变为模块化环境即可
操作方式:在 .ts 文件中添加 export {}
解释 1:当 TS 看到 export 这样的模块化语法后,就会将该文件当做模块来解析,此时,再声明的变量就是该模块中的局部变量了
解释 2:export 不需要导出任何内容,该代码的作用仅仅是为了让 TS 知道这是模块化环境
TypeScript 常用类型
TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了:类型系统
JS 有类型(比如,number/string 等),但是 JS 不会检查变量的类型是否发生变化,而 TS 会检查
TypeScript 类型系统的主要优势:可以显示标记出代码中的意外行为,从而降低了发生错误的可能性
类型注解
示例代码
1 | let age: number = 18; |
说明:代码中
:number就是类型注解
类型注解约束了只能给该变量赋值该类型的值
错误演示
1 | // 错误原因:将 string 类型的值赋值给了 number 类型的变量,类型不一致 |
常用基础类型
可以将 TS 中的常用基础类型分为两类
- JavaScript 已有类型
- 原始类型: number/string/boolean/null/undefined/symbol
- 对象类型:object(数组、对象、函数等)
- TypeScript 新增类型
- 联合类型、自定义类型(类型别名)、接口、元祖、字面量类型、枚举、void、any 等
注意:原始类型在 TS 和 JS 中写法一致, 对象类型在 TS 中更加细化,每个具体对象都有自己的类型语法
原始类型
number/string/boolean/null/undefined/symbol
特点:可完全按照 JavaScript 中的名称来书写
1 | let age: number = 18; |
数组类型
数组两种写法
类型[]写法, 如
1 | let userList: string[] = ['John', 'Bob', 'Tony']; |
- Array<类型>写法, 如
1 | let user2List: Array<string> = ['John', 'Bob', 'Tony']; |
联合类型
组中既有 number 类型,又有 string 类型,这个数组的类型应该如何写?
可以用|(竖线)分割多个类型, 如
1 | let str: string | number = 1; |
如果数组中可以是字符串或者数字,则可以这么写
1 | let arr1: Array<number | string> = [1, 2, '张三']; |
交叉类型
既满足a类型也满足b类型
可以用&(竖线)分割多个类型,通常是多个对象的交叉, 如
1 | interface Ikun { |
类型别名
当一个复杂类型或者联合类型过多或者被频繁使用时,可以通过类型别名来简化该类型的使用
用法:type 名称 = 具体类型
1 | type CustomArray = Array<number | string>; |
以上代码中,type作为创建自定义类型的关键字
- 类型别名可以使任意合法的变量名称
- 推荐大驼峰的命名写法
never类型
目标:能够知道 TS 中 never 类型的含义
内容:一般用于封装框架或工具库时使用
never类型:永远不会出现的值的类型(或永远不会发生的类型)never类型:处理 TS 类型系统的最底层- 可以将 never 类型的数据,赋值给任意其他的类型;无法将任何类型(除了 never 类型自己)来分配给 never 类型
- 当遇到never类型时,表示此类型不能用,不能通过该类型来解释当下的运行环境,必须指明类型
1 | // 函数抛出错误,就会结束函数的指向,函数不会有返回值的,这种函数的返回值就是never类型 (不存在的类型) |
unknown类型
目标:能够知道 TS 中的 unknown 类型
内容:
unknown类型是类型安全的 any- 可以把任何类型的值赋值给 unknown 类型的变量(别的类型可以赋值给unknown类型)
- 注意:不可以把unknown类型的变量 赋值给 任意类型的值 (除去unknown类型),自己可以是任意值,但是不可以给别人;
- 在使用 unknown 类型前,必须先将其设置为一个具体的类型(可以使用
typeof进行类型校验/缩小),否则,无法对其进行任何操作
1 | let e: unknown |
any类型
- 一旦设置为any类型,ts就失去了类型约束的作用
- 任何值可以赋值给any,any也可以赋值给任何值,且原有的类型也会变成any
对比 any 和 unknown 类型:
- 对于 any 类型来说,TS 不会对其进行类型检查
1 | // 可以进行任意操作,没有安全可言 |
- unknown 类型
1 | let value: unknown; |
never类型和unknown类型
- never 处理 TS 类型系统的最底层
- 无法将任意类型的数据赋值给 never 类型
- 可以将 never 类型赋值给任意其他类型
- 理解:never 表示啥也不是,所以,无法给 never 设置任何内容
- 理解:never 处于最底层,相当于任何类型的子类型,所以,可以赋值给其他任何类型
- 比如,’a’ 字面量就是 string 的子类型
let s: string = 'a'
- 比如,’a’ 字面量就是 string 的子类型
- unknown 处于 TS 类型系统的最顶层
- 可以将任意类型的数据赋值给 unknown 类型
- 无法将 unknown 类型赋值给任意其他类型
- 简单来说:unknown 类型可以接受任意类型,但是无法赋值给其他类型
- 理解:unknown 表示不确定,不确定就可以表示任意类型,既然可以是任意类型,所以可以接受任意类型的数据
- 理解:unknown 表示不确定,不确定就可以表示任意类型,既然可以是任意类型,所以就无法赋值给一个特点的类型
1 | // 头部 底部 unknown -> ... -> ...-> never |
1 | let u: unknown; |
函数类型
除了变量,我们常见的类型指定还有针对函数的类型声明
函数类型需要指的是 函数参数和返回值的类型,这里分为两种写法
- 第一种: 单独指定参数,返回值类型
1 | // 单独指定函数返回值和函数参数 |
- 第二种, 同时指定参数和返回值
1 | // 同时指定参数和返回值 |
注意: 当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型,这种形式只适用于函数表达式
void 类型
当我们的函数定义为没有返回值的类型时,可用关键字void表示
1 | // 没有返回值的函数 |
如果一个函数没有返回值,此时,在 TS 的类型中,应该使用 void 类型
1 | // 如果什么都不写 表示add4函数的类型为void |
函数可选参数
当我们定义函数时,有的参数可传可不传,这种情况下,可以使用 TS 的可选参数来指定类型
比如,在使用数组的slice方法时,我们可以直接使用slice() 也可以传入参数 slice(1) 也可以slice(1,3)
1 | const slice = (start?: number, end?: number): void => {}; |
? 表示该参数或者变量可传可不传
注意:可选参数只能出现在参数列表的最后, 即必须参数必须在可选参数之前
调用签名
如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature)
1 | interface ICalcFn { |
注意:在参数列表和返回的类型之间用的是 : 而不是 =>
构造签名
1 | class Person {} |
函数的重载
需求: 在TypeScript中,如果我们编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
在TypeScript中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用;
一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;
1 | // ts中函数的重载写法 |
this的内置工具使用
1 | function foo(this: { name: string }, info: {name: string}) { |
对象类型
JS 中的对象是由属性和方法组成的,TS 的对象类型是对象中属性和方法的描述
{} 用来指定对象中可以包含那些属性;
语法:{属性名:属性值,属性名:属性值}
1 | // 如果有多个属性 可以换行 去掉间隔符号 |
总结:
- 可是使用
{}来描述对象结构- { } 里的属性必须有,不能多也不能少
- 使用
?表示可选属性,可写可不写 [propName:string]:any,表示任意类型的属性(propName是变量名,可以随便写)
- 属性采用
属性名:类型形式 - 函数可以采用
方法名(): 返回值类型或者函数名: Function(不指定返回值)的形式
使用类型别名
直接使用{}会降低代码可读性,不具有辨识度,更推荐使用类型别名type添加对象类型
1 | type PersonObj = { |
带有参数的方法的类型
如果对象中的函数带有参数,可以在函数中指定参数类型
1 | // 带参数的函数方法 |
箭头形式的方法类型
语法:(形参:类型,形参:类型,…) => 返回值
1 | // 箭头函数形式定义类型 |
对象可选属性
对象中的若干属性,有时也是可选的,此时我们依然可以使用?来表示
1 | type Config = { |
接口 interface
当一个对象类型被多次使用时,一般使用接口(interface)描述对象的类型,达到复用的目的
- 我们使用
interface关键字来声明接口 - 接口名称推荐以
I为开头 - 声明接口之后,直接使用接口名称作为变量的类型
接口后不需要分号
1 | // 接口:接口后面的分号(;)可以不写、接口也可以使用?定义可选属性 |
接口和自定义类型的区别
相同点:都可以给对象指定类型
不同点: 接口只能为对象指定类型, 类型别名可以为任意类型指定别名
- 推荐用 type 来定义
接口继承
- 如果两个接口之间有相同的属性和方法,可以讲公共的属性和方法抽离出来,通过继承来实现复用
比如,这两个接口都有 x、y 两个属性,重复写两次,可以,但很繁琐
1 | interface Point2D { |
- 更好的方式
1 | interface Point2D { x: number; y: number } |
我们使用extends关键字实现了 Point3D 继承了 Point2D 的所有属性的定义, 同时拥有继承的属性和自身自定义的属性
元组
元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型
语法:[类型,类型…] , 一般不会特别长
当我们想定义一个数组中具体索引位置的类型时,可以使用元祖。
原有的数组模式只能宽泛的定义数组中的普遍类型,无法精确到位置
元组是另一种类型的数组,它确切知道包含多少个元素,以及特定索引对应的类型
1 | let position: [number, number] = [39.5427, 116.2317]; |
1 | // 所用于定义函数的返回值 |
类型推论
在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型
也就是说,由于类型推论的存在,在某些地址类型注解可以省略不写。
- 发生类型推论的常见场景
- 声明变量并初始化时
- 决定函数返回值时
1 | // 变量creater_name自动被推断为 string |
推荐:能省略类型注解的地方就省略(偷懒,充分利用 TS 类型推论的能力,提升开发效率)
技巧:如果不知道类型,可以通过鼠标放在变量名称上,利用 VSCode 的提示来查看类型
字面量类型
下面的代码类型分别是什么?
1 | // 字面量类型 |
通过 TS 的类型推导可以得到答案
变量 str1 的变量类型为: string
变量 str2 的变量类型为: ‘张三’
解释:str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string
str2 是一个常量(const),它的值不能变化只能是 ‘张三’,所以,它的类型为:”张三”
此时,”张三” 就是一个字面量类型,即某个特殊的字符串也可以作为 TS 中的类型
任意的 JS 字面量(对象,数组,数字)都可以作为类型使用
使用场景和模式
- 使用模式:字面量类型配合联合类型一起使用
- 使用场景:用来表示一组明确的可选值列表
- 比如,在贪吃蛇游戏中,游戏的方向的可选值只能是上、下、左、右中的任意一个
1 | type Direction = 'left' | 'right' | 'up' | 'down'; |
- 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个
- 优势:相比于 string 类型,使用字面量类型更加精确、严谨
枚举
- 枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值
- 枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个
1 | // 枚举 |
数字枚举
- 问题:我们把枚举成员作为了函数的实参,它的值是什么呢?
- 解释:通过将鼠标移入 Direction.Up,可以看到枚举成员 Up 的值为 0
- 注意:枚举成员是有值的,不赋值时默认为:从 0 开始自增的数值
- 我们把枚举成员的值为数字的枚举,称为:
数字枚举 - 当然,也可以给枚举中的成员初始化值
1 | // Down -> 11、Left -> 12、Right -> 13 |
字符串枚举
- 字符串枚举:枚举成员的值是字符串
- 注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
1 | enum Direction { |
枚举实现原理
- 枚举是 TS 为数不多的非 JavaScript 类型级扩展(不仅仅是类型)的特性之一
- 因为:其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)
- 也就是说,其他的类型会在编译为 JS 代码时自动移除。但是,枚举类型会被编译为 JS 代码
1 | enum Direction { |
- 说明:枚举与前面讲到的字面量类型+联合类型组合的功能类似,都用来表示一组明确的可选值列表
- 一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效
any 类型
- 原则:不推荐使用 any!这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)
- 因为当值的类型为 any 时,可以对该值进行任意操作,并且不会有代码提示
1 | let obj: any = { x: 0 }; |
- 解释:以上操作都不会有任何类型错误提示,即使可能存在错误
- 尽可能的避免使用 any 类型,除非临时使用 any 来“避免”书写很长、很复杂的类型
- 其他隐式具有 any 类型的情况
- 声明变量不提供类型也不提供默认值
- 函数参数不加类型
- 注意:因为不推荐使用 any,所以,这两种情况下都应该提供类型
在项目开发中,尽量少用 any 类型
类型断言
有时候你会比 TS 更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型。 比如,
1 | // 页面中一个a标签,a标签的id是link,获取这个a标签的dom元素 |
- 注意:该方法返回值的类型是 HTMLElement,该类型只包含所有标签公共的属性或方法,不包含 a 标签特有的 href 等属性
- 因此,这个**类型太宽泛(不具体)**,无法操作 href 等 a 标签特有的属性或方法
- 解决方式:这种情况下就需要使用类型断言指定更加具体的类型
- 使用类型断言:
1 | const aLink = document.getElementById('link') as HTMLAnchorElement; |
- 解释:
- 使用
as关键字实现类型断言 - 关键字 as 后面的类型是一个更加具体的类型(HTMLAnchorElement 是 HTMLElement 的子类型)
- 通过类型断言,aLink 的类型变得更加具体,这样就可以访问 a 标签特有的属性或方法了
- 类型断言是主观判断,断言开发者主观认为它一定是一种确定的类型 (主观行为,会屏蔽ts的错误提示)
- 使用
- 另一种断言语法,使用
<类型>变量语法,这种语法形式不常用知道即可:
1 | // 该语法,知道即可:在react的jsx中使用会报错 |
断言总结
- 类型断言:变量 as 类型 (强制转化类型)
1 | type CustomObj1 = { |
- 非空断言:变量! (变量后面跟一个感叹号!,表示主观认为变量不为空)
- 常量断言:变量 as const (表示变量是一个常量或者字面量)
类型缩小
1、typeof
- 众所周知,JS 中提供了 typeof 操作符,用来在 JS 中获取数据的类型
1 | console.log(typeof 'hello world') // .js文件 和 .ts文件里面都会输出:string |
- 实际上,TS 也提供了 typeof 操作符:可以在类型上下文中引用变量或属性的类型(类型查询)
- 使用场景:根据已有变量的值,获取该值的类型,来简化类型书写
1 | let p = { x: 1, y: 2 }; |
- 解释:
- 使用
typeof操作符来获取变量 p 的类型,结果与第一种(对象字面量形式的类型)相同 - typeof 出现在类型注解的位置(参数名称的冒号后面)所处的环境就在类型上下文(区别于 JS 代码)
- 注意:typeof 只能用来查询变量或属性的类型,无法查询其他形式的类型(比如,函数调用的类型)
- 使用
2、平等缩小
我们可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != )
1 | type Direction = "left" | "right" | "up" | "down" |
3、instanceof
1 | function printDate(date: string | Date) { |
4、in操作符 (索引类型)
1 | // 使用索引类型 |
TypeScript 高级类型
TypeScript面向对象
TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测
类的定义
使用class关键字来定义一个类
我们可以声明类的属性:在类的内部声明类的属性以及对应的类型
- 如果类型没有声明,那么它们默认是any的
- 我们也可以给属性设置初始化值
- 在默认的strictPropertyInitialization模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错
- 如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用
name!: string语法
- 如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用
1 | class Person { |
类的继承
我们使用extends关键字来实现继承,子类中使用super来访问父类。
1 | class Student extends Person { |
类的成员修饰符
public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;private 修饰的是仅在同一类中可见、私有的属性或方法;protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
readonly 只读属性,外接无法修改
1 | class Person { |
getters/setters
私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器,多用于对属性的访问进行拦截操作
1 | class Person { |
参数属性
TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性
- 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符
当类中有多个属性时,可以使用参数属性进行简写
1 | class Person { |
抽象类abstract
- 我们知道,继承是多态使用的前提
- 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式(多态:父类引用指向子类对象)
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法
- 什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法
- 抽象方法,必须存在于抽象类中
- 抽象类是使用abstract声明的类
- 抽象类有如下的特点
- 抽象类是不能被实例的话(也就是不能通过new创建)
- 抽象方法必须被子类实现
演练:封装一个通用的获取面积的方法
1 | abstract class Shape { |
类的作用
1、可以创建类对应的实例对象
2、类本身可以作为这个实例的类型
3、类也可以当做有一个构造签名的函数
索引签名
定义:有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征,这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型;
一个索引签名的属性类型必须是 string 或者是 number。
- 虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型
1 | interface ICollection { |
接口继承
可以从其他的接口中继承过来属性
- 减少了相代码的重复编写
- 如果使用第三库,给我们定义了一些属性,如果我们自定义一个接口,同时你希望自定义接口拥有第三方某一个类型中所有的属性,可以使用继承来完成
接口支持多继承,类不支持多继承
1 | interface Person { |
接口的实现
使用关键字implements
1 | interface IKun { |
严格字面量赋值检测
1 | interface IPerson { |

TS 中的类型兼容性
目标:能够理解 TS 中的类型兼容性
内容:
两种类型系统:1. Structural Type System(结构化类型系统) 2. Nominal Type System(标明类型系统)
TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状
也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。比如:
1 | interface Point { |
对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y(成员多的可以赋值给成员少的,或者说:只要满足必须的类型就行,多了也没事)
1 | interface Point2D { |
对于函数类型来说,类型兼容性比较复杂,需要考虑:1.参数个数、2.返回值类型 等等
参数个数:参数多的兼容参数少的 (或者说,参数少的可以赋值给多的)
- 在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性
1 | const arr = ['a', 'b', 'c']; |
- 返回值类型:只要满足必须的类型要求就行,多了也没事
1 | // 这种情况不会报错 |
泛型
类型参数化
内容:
- 泛型(Generics)可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class 中
- 需求:创建一个 id 函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)
1 | // 比如,该函数传入什么数值,就返回什么数值 |
- 比如,id(10) 调用以上函数就会直接返回 10 本身。但是,该函数只接收数值类型,无法用于其他类型
- 为了能让函数能够接受任意类型的参数,可以将参数类型修改为 any。但是,这样就失去了 TS 的类型保护,类型不安全
1 | function id(value: any): any { |
- 这时候,就可以使用泛型来实现了
- 泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用
- 实际上,在 C# 和 Java 等编程语言中,泛型都是用来实现可复用组件功能的主要工具之一
1 | // 定义数组的两种方式,下面的这种方式就是使用泛型来定义的数组的 |
泛型函数
内容:
创建泛型函数:
1 | // 注意:<Type>泛型是写在函数(value: Type)这个括号的前面的 (而不是之前的理解的写在函数名id后面的) |
解释:
- 语法:在函数名称的后面添加
<>(尖括号),尖括号中添加类型变量,比如此处的 Type - 类型变量 Type,是一种特殊类型的变量,它处理类型而不是值
- 类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)
- 因为 Type 是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型
- 类型变量 Type,可以是任意合法的变量名称
调用泛型函数:
1 | // 调用上面使用了泛型创建的id函数 |
解释:
- 语法:在函数名称的后面添加
<>(尖括号),尖括号中指定具体的类型,比如,此处的 number - 当传入类型 number 后,这个类型就会被函数声明时指定的类型变量 Type 捕获到
- 此时,Type 的类型就是 number,所以,函数 id 参数和返回值的类型也都是 number
- 这样,通过泛型就做到了让 id 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全
简化泛型函数调用
内容:
在调用泛型函数时,可以省略 <类型> 来简化泛型函数的调用
1 | // 省略 <number>、<string>泛型,直接调用函数,可以进行类型推断出具体类型 |
解释:
- 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量 Type 的类型
- 比如,传入实参 10,TS 会自动推断出变量 num 的类型 number,并作为 Type 的类型
- 推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读
- 说明:当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数
1 | // 数组的一些类型推断 |
泛型接口
泛型接口:接口也可以配合泛型来使用,以增加其灵活性,增强其复用性
1 | interface IdFunc<Type> { |
解释:
- 在接口名称的后面添加
<类型变量>,那么,这个接口就变成了泛型接口。 - 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。
- 使用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc)。
- 此时,id 方法的参数和返回值类型都是 number;ids 方法的返回值类型是 number[]。
实际上,JS 中的数组在 TS 中就是一个泛型接口
1 | // 数组的forEach方法,使用泛型定义的,如果知道数组的类型,就可以根据类型推断,推断出参数的具体类型 |
- 解释:当我们在使用数组时,TS 会根据数组的不同类型,来自动将类型变量设置为相应的类型
- 技巧:可以通过 Ctrl + 鼠标左键(Mac:Command + 鼠标左键)来查看具体的类型信息
泛型约束
目标:能够知道为什么要为泛型添加约束
内容:
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:
- 比如string和array都是有length的,或者某些对象也是会有length属性的;
- 那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢?
比如,以下示例代码中想要获取参数的长度:
- 因为 Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length。因此,无法访问 length 属性
1 | function id<Type>(value: Type): Type { |
此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)
添加泛型约束收缩类型,主要有以下两种方式:1.指定更加具体的类型 2.添加约束
首先,我们先来看第一种情况,如何指定更加具体的类型:
比如,将类型修改为 Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性,因此就可以访问了
1 | function id<Type>(value: Type[]): Type[] { |
添加泛型约束
目标:能够使用 extends 关键字来为泛型函数添加类型约束
内容:
1 | // 创建一个自定义类型 |
解释:
- 创建描述约束的接口 ILength,该接口要求提供 length 属性
- 通过
extends关键字来为泛型(类型变量)添加约束 - 该约束表示:传入的类型必须具有 length 属性
- 注意:传入的实参(比如,数组/字符串)只要有 length 属性即可,除了length可以有别的属性(类型兼容性)
多个类型变量的泛型 (keyof)
目标:能够知道泛型可以有多个类型变量
内容:
泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束) 比如,创建一个函数来获取对象中属性的值:
1 | function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) { |
解释:
- 添加了第二个类型变量 Key,两个类型变量之间使用
,逗号分隔。 - keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。
- 本示例中
keyof person实际上获取的是 person 对象所有键的联合类型,也就是:'name' | 'age' - 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性
1 | // Type extends object 表示:Type 应该是一个对象类型,如果不是 对象 类型,就会报错 |
1 | // Redux 整个应用,状态的类型 |
泛型工具类型
泛型工具类型:TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作
说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。 这些工具类型有很多,主要学习以下几个:
Partial<Type>Readonly<Type>Pick<Type, Keys>Record<Keys, Type>Required<Type>ReturnType<Type>Omit<Type, Keys>Parameters<Type>NonNullable<Type>Exclude<UnionType, ExcludedMembers>Extract<Type, Union>InstanceType<Type>ConstructorParameters<Type>
Partial
- Partial 用来构造(创建)一个类型,将 Type 的所有属性都变成为可选属性。
1 | type Props = { |
- 解释:构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的。
Readonly
- Readonly 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)。
1 | type Props = { |
- 解释:构造出来的新类型 ReadonlyProps 结构和 Props 相同,但所有属性都变为只读的。
1 | let props: ReadonlyProps = { id: '1', children: [] }; |
- 当我们想重新给 id 属性赋值时,就会报错:无法分配到 “id” ,因为它是只读属性。
Pick
- Pick<Type, Keys> 从 Type 中选择一组属性来构造新类型。
1 | interface Props { |
- 解释:
- Pick 工具类型有两个类型变量:1. 表示选择谁的属性 2. 表示选择哪几个属性。
- 其中第二个类型变量,如果只选择一个则只传入该属性名即可,如果有多个使用联合类型即可。
- 第二个类型变量传入的属性只能是第一个类型变量中存在的属性。
- 构造出来的新类型 PickProps,只有 id 和 title 两个属性类型。
Record
1 | // 使用场景:如果已经知道对象中键的集合,可以直接通过 Record 来快速创建一个对象类型 |
Required
1 | // 把传入的类型变为必填状态 |
ReturnType
1 | // 该类型能够获取函数类型 T 的返回值类型 |
Omit
Omit <T, K>在 T 类型中删除对应 K 属性
1 | type IPerson = { |
Parameters
1 | // 返回的是函数类型 fn 的形参元组 |
NonNullable
1 | // 从T中剔除null和undefined:去除掉联合类型中的 null 和 undefined 类型 |
Exclude
Exclude<T, U>最常用的还是结合两个联合类型来使用的,我们能通过Exclude取出T联合类型在U联合类型中没有的子类型 — 从T中排除可分配给U的类型
1 | interface Person { |
Extract
Extract<T, U>和Exclude<T, U>是相反的,最常用的还是结合两个联合类型来使用的,我们能通过Extract取出T联合类型在U联合类型中所有重复的子类型 — 从T中提取可分配给U的类型
1 | interface Person { |
InstanceType
- 获取 class 构造函数的返回类型
1 | class Person { |
ConstructorParameters
1 | // 获取构造函数中参数类型 元组 形式 |
映射类型
内容:
有的时候,一个类型需要基于另外一个类型,但是你又不想拷贝一份,这个时候可以考虑使用映射类型
- 大部分内置的工具都是通过映射类型来实现的;
- 大多数类型体操的题目也是通过映射类型完成的;
说明:映射类型只能使用type定义
映射类型建立在索引签名的语法上:
- 映射类型,就是使用了 PropertyKeys 联合类型的泛型
- 其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型;
1 | interface Iperson { |

映射修饰符
在使用映射类型时,有两个额外的修饰符可能会用到:
- 一个是 readonly,用于设置属性只读;
- 一个是 ? ,用于设置属性可选;
你可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀。
解释:
默认是
+,使用了映射修饰符,拷贝出来的类型所有的属性都会添加上对应的修饰符原始类型中可能有?或 readonly,使用了
-,在拷贝出来的类型中?或 readonly会被去掉
1 | type MapPerson<Type> = { |

TypeScript知识扩展
TypeScript模块化
我们需要先理解 TypeScript 认为什么是一个模块。
- JavaScript 规范声明任何没有 export 的 JavaScript 文件都应该被认为是一个脚本,而非一个模块
- 在一个脚本文件中,变量和类型会被声明在共享的全局作用域,将多个输入文件合并成一个输出文件,或者在 HTML使用多
个
