第 1 章 导言
使用 TypeScript 开发的程序更安全,常见的错误都能检查出来,写出的代码还可以作为文档。
更安全是指类型安全
类型安全:借助类型避免程序做无效的事情(无效指的是运行时程序崩溃或未崩溃,但做的事情毫无意义
举个 🌰
- 数字乘以一个列表
- 接收数字的函数却传入了字符串
- 调用对象上不存在的方法
- 导入已经被移除的模块
1
2
3
4
5
6
7
8
9
10
113 + [] // "3"
let obj = {}
obj.foo // undefined
function a(b) {
return b/2
}
a('z') // NaN
在做无效事情的时候,JavaScript 没有抛出异常,而是尽自己所能,避免抛出异常。
而 JavaScript 这种特性让代码中错误的产生与发现脱节了。导致 bug 往往是由他人转告给你的。
到真正运行时可能才会发现错误。
而 TypeScript 给出错误的时间点:在输入代码的过程中,代码编辑器会给出错误消息,来提醒你。
1 | 3 + [] // Error TS2365: Operator '+' cannot be applied to types '3' and 'never[]'. |
第 2 章 TypeScript 概述
编译器
TypeScript 编译器(TSC)
通常运行程序的大致流程
- 把程序解析为 AST
- AST 编译成字节码
- 运行时计算字节码
运行程序就是让运行时计算由编译器从源码解析得来的 AST 生成的字节码。
TypeScript 的特殊之处在于,不直接编译成字节码,而是编译成 JavaScript。然后再像往常一样,在浏览器/NodeJS 中运行。
TypeScript 编译器生成 AST 之后,真正运行代码之前会对代码做类型检查。
类型检查器:检查代码是否符合安全要求的特殊程序
编译和运行 TypeScript (1-3 由 TSC 操作,4-6 由浏览器/NodeJS 操作)
- TypeScript 源码 -> TypeScript AST
- 类型检查器检查 AST
- TypeScript AST -> JavaScript 源码
- JavaScript 源码 -> JavaScript AST
- AST -> 字节码
- 运行时计算字节码
类型只在类型检查这一步使用,TSC 把 TS 编译成 JS 时,不会考虑类型。可以确保可以随意改动、更新和改进程序中的类型,而无需担心会破坏应用的功能。
类型系统
类型系统:类型检查器为程序分配类型时使用的一系列规则
一般来说,类型系统有两种,各有利弊
- 通过显式句法告诉编译器所有值的类型
- 自动推导值的类型
JavaScript 在运行时推导类型
TypeScript 身兼两种类型系统,可以显式注解类型,也可以自动推导多数类型。
显示声明类型需要使用注解。注解的形式 value: type
,就像是告诉类型检查器,“嘿,看到这个 value 了吗?它的类型是 type。”
1 | // 显示注解 |
TypeScript VS JavaScript
类型系统特性 | JavaScript | TypeScript |
---|---|---|
类型是如何绑定的? | 动态 | 静态 |
是否自动转换类型? | 是 | 否(多数时候) |
何时检查类型? | 运行时 | 编译时 |
何时报告错误? | 运行时(多数时候) | 编译时(多数时候) |
TypeScript 能做的是把纯 JavaScript 代码中那些运行时愈发和类型相关的错误提前到编译时报告。在代码编辑器中显示,输入代码后立即就有反馈。
类型是如何绑定的?
JavaScript 动态绑定类型,必须运行程序才能知道类型。
TypeScript 渐进式类型语言,在编译时知道所有类型
第 3 章 类型全解
类型:一系列值及对其执行的操作
example
类型 | 包含的值 | 可以执行的操作 |
---|---|---|
boolean | true、false | ||、&&、! |
number | 所有数字 | +、-、*、/、%、&&、? .toFixed()、.toString() |
string | 所有字符串 | +、||、&& .concat()、.toUpperCase() |
对 T 类型的值来说,我们不仅知道值的类型是 T,还知道可以/不可以对该值做什么操作。
类型检查器通过使用的类型和具体用法判断是否有效。
TypeScript 的类型层次结构
类型术语
- 类型注解(可以理解为某种界限
1
2
3
4function squareOf(n: number) {
return n * n;
}
squareOf(2); // 4
类型浅谈
any
在 TypeScript 中,编译时一切都要有类型,如果你和 TypeScript(类型检查器)无法确认类型是什么,默认为 any。这是兜底类型,应该尽量避免使用。
类型的定义(一系列值及可以对其执行的操作)any 包含所有值,而且可以对其做什么操作。any 类型的值就像常规的 JavaScript 一样,类型检查器完全发挥不了作用。
使用 any 需要显示注解。
tsconfig.json
noImplicitAny: true;
noImplicitAny 隶属于 TSC 的 strict 标志家族,
unknown
unknown 与 any 类似,也表示任何值。但是 TypeScript 会要求你在做检查,细化类型。
类型 | 包含的值 | 可以执行的操作 |
---|---|---|
unknown | == 、=== 、|| 、&& 、? 、! 、typeof 、instance of |
1 | // example |
unknown 的用法
- TypeScript 不会把任何值推导为 unknown 类型,必须显示注解(a)
- unknown 类型的值可以比较(b)
- 执行操作时不能假定 unknown 类型的值为某种特定类型(c),必须先向 TypeScript 证明一个值确实是某个类型(d)
boolean
类型 | 包含的值 | 可以执行的操作 |
---|---|---|
boolean | true 、false |
== 、=== 、|| 、&& 、? |
1 | // example |
- TypeScript 推导出值的类型为 boolean(a 和 b)
- 使用 const,让 TypeScript 推导出值为某个具体的布尔值(c)
- 显式注解,声明值的类型为 boolean(d)
- 显式注解,声明值为某个具体的布尔值(e 和 f)。把类型设定为某个值,就限制了 e 和 f 在所有布尔值中只能取指定的那个值。这种特性被称为类型字面量。
类型字面量——仅表示一个值的类型
变量 e f 是使用类型字面量显示注解了变量,变量 c 则是由 TypeScript 推导出一个字面量类型,因为使用的是 const。
const 声明的基本类型的值,赋值之后无法修改,因此 TypeScript 推导出的是范围最窄的类型,所以 TypeScript 推导出的 c 的类型为 true,而不是 boolean。
number
类型 | 包含的值 | 可以执行的操作 |
---|---|---|
number | 整数、浮点数、正数、负数、Infinity、NaN 等 | 算术运算 比较 |
1 | // example |
- TypeScript 推导出值的类型为 number(a 和 b)
- 使用 const,让 TypeScript 推导出值为某个具体的数字(c)
- 显式注解,声明值的类型为 number(e)
- 显式注解,声明值为某个具体的数字(f 和 g)
tips:处理较长的数字时可以使用数字分隔符。
1 | let oneMillion = 1_000_ 000 // 等同于 1000000 |
bigint
是 JavaScript 和 TypeScript 新引入的类型,在处理较大的整数时,不用再担心舍入误差。
number 类型表示的整数最大为 253,bigint 可以表示任意大的整数。
类型 | 包含的值 | 可以执行的操作 |
---|---|---|
bigint | 所有 BigInt 数 | 算术运算 比较 |
1 | // example |
与 boolean 和 number 一样,声明 bigint 类型也有四种方式。尽量让 TypeScript 自动推导。
string
类型 | 包含的值 | 可以执行的操作 |
---|---|---|
string | 所有字符串 | 字符串可以进行的操作 例如 +、.slice() |
1 | // example |
同样也是尽量让 TypeScript 自动推导 string 类型。
symbol
symbol 经常用于代替对象和映射的字符串健,防止被意外设置。
symbol 的类型就是 symbol,每一个 symbol 都是唯一的,不与其他任何符号相等,即便再使用相同的名称创建一个 symbol 也是如此。
1 | // example |
1 | // example |
创建 symbol 的方式
- 使用 const,TypeScript 会推导为 unique symbol 类型。
- 显式注解 const 变量的类型为 unique symbol
- unique symbol 类型的值始终与自身相等
- TypeScript 在编译时知道一个 unique symbol 绝对不会与另一个 unique symbol 相等
unique symbol 与其他字面量类型其实是一样的。
对象
TypeScript 的对象类型表示对象的结构。
结构化类型–一种编程设计风格,只关心对象有哪些属性,而不管属性使用什么名称(名义化类型)。在某些语言中也叫鸭子类型(即不以貌取人)
1 | // example |
object 只能表示该值是一个 JavaScript 对象(而且不是 null)
1 | // 对象字面量 |
对象字面量句法的意思是,“这个东西的结构是这样过的。”
使用 const 声明对象不会导致 TypeScript 把推导的类型缩窄。与上面的基本类型不同。这是因为 JavaScript 对象是可变的,所以在 TypeScript 看来,创建对象之后你可能会更新对象的字段。
1 | let a: { b: number } |
默认情况下,TypeScript 对对象的属性要求十分严格。如果声明对象有个类型为 number 的属性 b,TypeScript 将预期对象有且只有这个属性。缺少或者多了,TypeScript 都会报错。
1 | let a: { |
[key: T]: U
句法称为索引签名,通过这种方式告诉 TypeScript,指定的对象可能有更多的 key。这种句法的意思是,“在这个对象中,类型为 T 的健对应的值为 U 类型。”
- 索引签名 key 的类型 T 必须可赋值给 number 或 string。(JavaScript 对象的健为字符串;数组是特殊的对象,健为数字。)
- key 的名称可以是任意词,不一定非的用 key
对象字面量表示法有一个特例:空对象类型 {}
。除 null 和 undefined 之外的任何类型都可以赋值给空对象类型,应该尽量避免使用。
在 TypeScript 中声明对象类型有四种方式
- 对象字面量表示法
{a: string}
,也称对象结构 - 空对象字面量表示法
{}
。避免使用 - object 类型。如果需要个对象,当对这个对象的字段没有要求,使用这种方式。
- Object。避免使用
对一个值,在类型允许的情况下,可以对其执行特定的操作。其实在类型自身上也可以执行一些操作。
类型别名
1 | type Age = number |
类型别名采用块级作用域。在同一作用于中不能重复声明相同类型。
并集和交集
1 | type Cat = { name: string, purrs: boolean } |
并集通常更常用
- 函数返回值可能是一个字符串,也可能是 null。
string | null
- 混合类型的数组
数组
1 | let a = [1, 2, 3] // number[] |
TypeScript 支持两种注解数组类型的句法
- T[]
- Array
一般情况下,数组应该保持同质。
元祖
array 的子类型,长度固定,各索引位上的值具有固定的已知类型。
声明元组时必须显式注解类型。
1 | let a: [number] = 1 |
元组也支持可选元素
1 | let trainFares: [number, number?][] = [ |
元组也支持剩余元素,即为元组定义最小长度
1 | // 字符串列表,至少有一个元素 |
只读数组和元祖
1 | let as: readonly number[] = [1, 2, 3] // readonly number[] |
null、undefined、void 和 never
| 类型 | 含义 |
| — | — |
| null | 缺少值 |
| undefined | 尚未赋值的变量 |
| void | 没有 return 语句的函数 |
| never | 永不返回的函数 |
1 | // 返回 never 的函数 |
never 是所有类型的子类型,可以赋值给其他任何类型。
枚举
枚举的作用是列举类型中包含的各个值。是一种无序数据结构,把键映射到值上。
枚举可以理解为编译时键固定的对象,访问键时,TypeScript 将检查指定的键是否存在。
枚举分为两种
- 字符串到字符串之间的映射
- 字符串到数字之间的映射
1 | enum Language { |
按约定,枚举名称为大写单数形式。枚举中的键也大写。
TypeScript 可以自动为枚举中的各个成员推导对应的数字,也可以手动设置。
1 | enum Language { |
枚举中的值访问方式和对象一样
1 | let myFirstLanguage = Language.Russian |
一个枚举可以分成几次声明,TypeScript 将自动把各部分合并在一起
1 | enum Language { |
meiju
小结
类型 | 子类型 |
---|---|
boolean | Boolean 字面量 |
bigint | BigInt 字面量 |
number | Number 字面量 |
string | String 字面量 |
symbol | unique symbol |
object | Object 字面量 |
数组 | 元组 |
enum | const enum |
第 4 章 函数
声明和调用函数
在 JavaScript 中,函数是一等对象。这意味着,可以向对象那样使用函数
- 可以赋值给变量
- 可以作为参数传给其他函数
- 可以作为函数的返回值
- 可以赋值给对象和原型
- 可以赋予属性
- 可以读取属性
TypeScript 通常会显示注解函数的参数
1 | function add(a: number, b: number) { |
返回类型能推导出来,不过也可以显示注解
1 | function add(a: number, b: number): number { |
TypeScript 中声明函数
1 | // 具名函数 |
除了函数构造方法,其他几种句法在 TypeScript 中都可以放心使用,能够保证类型安全。通常需要注解参数的类型,而返回类型不要求必须注解。
在调用函数时,TypeScript 将检查传入的实参是否于函数形参类型兼容。
可选参数和默认参数
可选参数必须在末尾
1 | function log(message: string, userId?: string) { |
默认参数更常用,默认参数可以自动类型推导。
1 | ``` |
例子中,数组元素的类型可以为 number,不过 filter 函数的作用应该更一般,可以筛选数字数组、字符串数字、对象数组等。
下面通过重载描述下函数签名
1 | type Filter = { |
object 无法描述对象的结构,访问数组中元素属性就会报错。
为了解决这种问题,就有了泛形参数
泛型参数——在类型层面施加约束的占位类型,也称多态类型参数,简称泛形
1 | // example |
这么做的意思是 Filter 使用了一个泛形参数 T,事先不知道具体类型是什么,调用的时候根据传入的参数推导 T 的类型。
知识点
- 泛形使用尖括号声明,可以把尖括号理解为 type 关键字,只不过声明的是泛形。
- 尖括号位置限制泛形作用域尖括号中可以声明任意多个以逗号分隔
- T 就是一个类型名称(类似变量名称),可以使用任意名称,通常会使用 T U V W
- 泛形可以理解为一种约束,把泛形 T 所在位置的类型约束为 T 类型
什么时候绑定泛型
声明泛形的位置不仅限定泛形作用域,还决定什么时候为泛形绑定具体的值
1 | // 1 在调用签名中声明 |
可以在什么地方声明泛形
1 | // 1 |
泛形别名
1 | ``` |
1 | ``` |