title: 《TypeScript编程》笔记
第1章 导言
类型安全:借助类型避免程序做无效的事情。
第2章 TypeScript概述
2.1 编译器
程序运行过程:
1.把程序解析为AST。
2.AST编译成字节码。
3.运行时计算字节码。
TypeScript编译器生成AST之后,真正运行代码之前,TypeScript会对代码做类型检查(检查代码是否符合类型安全要求的特殊程序)。
2.2 类型系统
类型系统:类型检查器为程序分配类型时使用的一系列规则。类型系统有两种,一种是通过显式句法告诉编译器所有值的类型,另一种自动推导值的类型。
TypeScript支持两种类型系统,可以显式注解类型,也可以让TypeScript推导多数类型。
注解的形式:value: type
。
一般来说,最好让TypeScript推导类型,少数情况下才显式注解类型。
JavaScript和TypeScript的类型系统区别
类型系统特性 | JavaScript | TypeScript |
---|---|---|
类型是如何绑定的? | 动态 | 静态 |
是否自动转换类型? | 是 | 否(多数时候) |
何时检查类型? | 运行时 | 编译时 |
何时报告错误? | 运行时(多数时候) | 编译时(多数时候) |
JavaScript是动态绑定语言(运行程序才能知道类型),TypeScript是渐进式编译类型语言(编译时对代码做类型检查)。
JavaScript是弱类型语言,会自动转换类型,而TypeScript发现无效的操作时则报错。如果必须类型转换,请明确表明你的意图。
TypeScript会对代码做静态分析,找出这类错误,在运行之前反馈给你。
2.3 代码编辑器设置
2.3.1 tsconfig.json
每个TypeScript项目都应该在根目录放一个名为tsconfig.json的文件,在该文件中定义要编译哪些文件、把文件编译到哪个目录中,以及在使用哪个版本的JavaScript运行。
可以使用./node_modules/.bin/tsc --init
生成tsconfig.json。
2.3.2 tslint.json
tslint.json为代码制定风格上的约定。
ts-node,直接编译和运行TypeScript代码。
typescript-node-starter,快速生成文件夹结构。
第3章 类型全解
3.1 类型术语
3.2类型浅谈
const和let对TypeScript的推导结果是有影响的。
3.2.1 any
这是兜底类型,应该尽量避免使用。
3.2.2 unknown
如果你无法预知一个值的类型,不要使用any,应该使用unknown。unknown也表示任何值,但是TypeScript会再要求你再做检查。
unknown的用法:
1.unknown类似可以比较,可以否定,可以使用typeof和instanceof运算符。
2.TypeScript不会把任何值推导为unknown类型,必须显式注解。
3.执行操作时不能假定unknown类型的值为某种特定类型,必须先向TypeScript证明一个值确实是某个类型。
3.2.3 boolean
该值的类型可以比较,可以否定。
3.2.4 number
number包括所有数字:整数、浮点数、正数、负数、Infinity、NaN等。
如果没有特殊原因,不要把值的类型显式注解 为number。
处理较长的数字时,建议使用数字分隔符。let oneMillion = 1_1000_000 // 等同于1000000
。
3.2.5 bigint
bigint是JavaScript和TypeScript新引入的类型,在处理较大的整数,不用再担心舍入误差。
number类型表示的整数最大为2^53,bigint能表示的数比这大得多。
尽量让TypeScript推导bigint类型。
3.2.6 string
尽量让TypeScript推导string类型。
3.2.7 symbol
symbol类型分为symbol和unique symbol。
let a = Symbol('a') // symbol
const b = Symbol('c') // typeof b
unique symbol类型的值始终与自身相等。
3.2.8 对象
JavaScript一般采用结构化类型(一种编程设计风格,只关心对象有哪些属性,而不管属性使用什么名称),TypeScript直接沿用。
使用类型描述对象的方式:
1.把一个值声明为object类型。
let a: object = {}
。
只能表示该值是一个JavaScript对象(而且不是null)。
2.对象字面量句法
让TypeScript推导对象的结构。let a = { b: 'x' }
。
使用const声明对象不会导致TypeScript把推导的类型缩窄。
let n = {
firstName: '123',
lastName: '456'
}
class Person {
constructor(public firstName: string, // public是this.firstName = firstName的简写形式
public lastName: string
) {}
}
n = new Person('czl', 'ltx')
console.log(n);
// [key: T]: U => 称为索引签名,通过这种方式告知TypeScript指定的对象可能有更多的键。在这个对象中,类型为T的键对应的值为U类型。
// 键的类型(T)必须可赋值给number或string。索引签名中键的名称可以是任何词,不一定非得用key。
// 推荐!
let a: {
b: number
c?: string
[key: number]: boolean
}
let b: {
[name: string]: string
}
let c: Record<string, string>
可以使用readonly
修饰符把字段标记为只读。
对象字面量法有一个特例:空对象类型({})。除了null和undefined之外的任何类型都可以赋值给空对象类型,使用起来比较复杂。
let danger: {}
danger = {}
danger = {x: 1}
danger = []
danger = 2
请尽量避免使用空对象类型。
3.Object。
这与{}的作用基本一样,最好也避免使用。
3.2.9 类型别名、并集和交集
类型别名
我们可以使用变量声明(let、const和var)为值声明别名,也可以为类型声明别名。
type Age = number
type Person = {
name: string
age: Age
}
let driver: Person = {
name: '张三',
age: 18
}
与let和const一样的是,类型别名采用块级作用域,每块代码都有自己的作用域。类型别名有助于减少重复输入复制的类型(DRY)。
并集类型和交集类型
TypeScript提供了特殊的类型运算符:并集使用|
,交集使用&
。
type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean, wags: boolean}
type CatOrDogOrBoth = Cat | Dog
type CatAndDog = Cat & Dog
let a: CatOrDogOrBoth = {
name: 'zhangsan',
purrs: true
}
a = {
name: 'lisi',
barks: true,
wags: true,
purrs: true
}
3.2.10 数组
TypeScript支持两种注解数组类型的句法:T[]
和Array<T>
。两者的作用和性能无异。
一般情况下,数组应该保持同质。保证数组中的每个元素都具有相同的类型。
let a = [1,2,3] // number[]
let b: (string | number)[] = [1, '2', 3]
3.2.11 元组
元组是array的子类型,是定义数组的一种特殊方式,长度固定,各索引位置上的值具有固定的已知类型。
let a: [number] = [1]
let b: [string, number, string] = ['1', 2, '3']
let c: [number, number?] = [1]
// 至少有一个元素
let friend: [string, ...string[]] = ['a', 'b', 'c']
元组类型能正确定义元素类型不同的列表,还能知晓该种列表的长度。这些特性使得元组比数组安全得多,应该经常使用。
只读数组和元组
若想更改只读数组,使用非变型方法(.concat、.slice),不能使用可变型方法(.push、.splice)。
let as: readonly number[] = [1,2,3]
let bs: readonly [number, string] = [1, '2']
3.2.12 null、undefined、void和never
在TypeScript中,undefined类型只有undefined一个值,null类型只有null一个值。
undefined的意思是尚未定义,而null表示缺少值。
void
是函数没有显示返回任何值时的返回类型,never
是函数根本不返回(例如函数抛出异常,或者永远运行下去)时使用的类型。
// 返回void的函数
function returnVoid() {
let a = 2 + 2
let b = 3 + 3
}
// 返回never的函数
function returnNever() {
throw TypeError('i a error')
}
// 返回undefined的函数
function returnUndefined() {
return undefined
}
never
是其他每个类型的子类型,是“兜底类型”,这意味着never
类型可赋值给其他任何类型。
类型 | 含义 |
---|---|
null | 缺少值 |
undefined | 尚未赋值的变量 |
void | 没有return语句的函数 |
never | 永不返回的函数 |
3.2.13 枚举
枚举的作用是列举类型中包含的各个值。是一种无序数据结构,把键映射到值上。
枚举分为两种:字符串到字符串之间的映射和字符串到数字之间的映射。
按约定,枚举名称为大写的单数形式。枚举中的键也为大写。
TypeScript可以自动为枚举中的各个成员推导对应的数字,我们也可以手动设置。
一个枚举可以分成几次声明,TypeScript将自动把各部分合并在一起。
enum Language {
Engilsh = 0,
Spanish = 1,
Chinese = 2
}
enum Language {
Russian = 200 + 300,
Japan // 501
}
enum Color {
Red = '#c10000',
Blue = '#007ac1',
White = 255
}
Language['Chinese'] // 2
Color.Blue // '#007ac1'
可以通过const enum指定枚举的安全子集
const enum Language {
Engilsh,
Spanish,
Chinese
}
Language.Chinese
Language[0] // Error,不允许反向范围
3.3 小结
多数类型都分一般和具体两种形式,后者是前者的子类型。
类型 | 子类型 |
---|---|
boolean | Boolean字面量 |
bigint | BigInt字面量 |
number | Number字面量 |
string | String字面量 |
symbol | unique symbol |
object | Object字面量 |
数组 | 元组 |
enum | const enum |
第4章 函数
4.1 声明和调用函数
TypeScript能推导出函数体中的类型,但是多数情况下无法推导出参数的类型,只在少数特殊情况下能根据上下文推导出参数的类型。
通常需要注解参数的类型,而返回类型不要求必须注解。
4.1.1 可选和默认的参数
声明函数的参数时,必要的参数在前面,随后才是可选的参数。(带默认值的参数不要求放在参数列表的末尾)。
4.1.2 剩余参数
一个函数最多只能有一个剩余参数,而且必须位于参数列表的最后。
4.1.3 call、apply和bind
要在tsconfig.json中启用strictBindCallApply选项。
4.1.4 注解this的类型
如果需要禁止在类方法以外使用this,启用TSLint的no-invalid-this规则。
如果函数使用this,请在函数的第一个参数中声明this的类型(放在其他参数之前)。this不是常规的参数,而是保留字,是函数签名的一部分。
function fancyDate(this: Date) {
return this.getDate()
}
fancyDate.call(new Date())
4.1.5 生成器函数
function* createFibonacciGennerator(): IterableIterator<number> {
let a = 0
let b = 1
while(true) {
yield a;
[a, b] = [b, a + b]
}
}
const iterator = createFibonacciGennerator()
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
4.1.6 迭代器
可迭代对象,有Symbol.iterator属性的对象,而且该属性的值为一个函数,返回一个迭代器。
迭代器,定义有next方法的对象,该方法返回一个具有value和done属性的对象。
// 手动定义迭代器
let numbers = {
*[Symbol.iterator]() {
for (let n = 1; n <= 10; n++) {
yield n
}
}
}
console.log([...numbers])
4.1.7 调用签名
(a: number, b: number) => number
,这是TypeScript表示函数类型的句法,也称调用签名。函数的调用签名只包含类型层面的代码,即只有类型,没有值。
调用签名没有函数的定义体,无法推导出返回类型,所以必须显式注解。
“类型层面代码”指只有类型和类型运算符的代码。而其他的都是“值层面代码”。
// function greet(name: string)
type Greet = (name: string) => void
const greet: Greet = (name) => {
console.log(name);
}
4.1.8 上下文类型推导
const time = (f: (index: number) => void, n: number) => {
for (let i = 0; i < n; i++) {
f(i)
}
}
time(n => console.log(n), 10)
4.1.9 函数类型重载
// 简写型调用签名
type Log = (message: string, userId?: string) => void
// 完整型调用签名
type Log = {
(message: string, userId?: string): void
}
// 函数重载
type Reserve = {
(from: Date, to: Date, destination: string): Reservation
(from: Date, destinantion: string): Reservation
}
// 描述函数的属性
type WarnUser = {
(warning: string): void
wasCalled: boolean
}
let warnUser: WarnUser = (warning: string) => {
...
}
4.2 多态
泛型参数,在类型层面施加约束的占位类型,也称多态类型参数。
泛型参数使用<>
声明。
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
T就是一个类型名称,如果愿意,可以使用任何其他名称。
只要可能就应该使用泛型,这样写出的代码更具一般性,可重复使用,并且简单扼要。
4.2.1 什么时候绑定泛型
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
4.2.2 可以在什么地方声明泛型
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
type Filter = <T>(array: T[], f: (item: T) => boolean) => T[]
type Filter<T> = (array: T[], f: (item: T) => boolean) => T[]
function filter<T>(array: T[], f: (item: T) => boolean): T[] {}
4.2.3 泛型指导
function map<T, U>(array: T[], f: (item: T) => U): U[] {...}
//隐式注解
map(['a', 'b', 'c'], _ => _ === 'a')
//显示注解泛型
map <string, boolean>(['a', 'b', 'c'], _ => _ === 'a')
4.2.4 泛型别名
// 在类型别名中只有这一个地方可以声明泛型
type MyEvent<T> = {
target: T,
type: string
}]
type ButtonEvent = MyEvent<HTMLButtonElement>
type TimedEevent<T> = {
event: MyEvent<T>
from: Date
to: Date
}
function triggerEvent<T>(event: MyEvent<T>): void {...}
triggerEvent({
target: document.querySelector('#myButton'),
type: 'mouseover'
})
4.2.5 受限的多态
type TreeNode = {
value: string
}
type LeafNode = TreeNode & {
isLeaf: true
}
type InnerNode = TreeNode & {
children: TreeNode | [TreeNode, TreeNode]
}
function mapNode<T extends TreeNode>(node: T, f: (value: string) => string): T {
return {
...node,
value: f(node.value)
}
}
let a1: TreeNode = { value: 'a' }
let b1: LeafNode = { value: 'b', isLeaf: true }
let c1: InnerNode = { value: 'c', children: b1 }
console.log(mapNode(a1, _ => _.toUpperCase())); // { value: 'A' }
console.log(mapNode(b1, _ => _.toUpperCase())); // { value: 'B', isLeaf: true }
console.log(mapNode(c1, _ => _.toUpperCase())); // { value: 'C', children: { value: 'b', isLeaf: true } }
有多个约束的受限多态
type HasSides = { numberOfSide: number }
type SideHaveLength = { sideLength: number }
function logPerimeter<Shape extends HasSides & SideHaveLength>(s: Shape): Shape {
console.log(s.numberOfSide * s.sideLength); // 12
return s
}
console.log(logPerimeter({ numberOfSide: 4, sideLength: 3 }));
使用受限的多态模拟变长参数
function call<T extends unknown[], R>(f: (...args: T) => R, ...args: T) : R {
return f(...args)
}
4.2.6 泛型默认类型
type MyEvent<T = HTMLButtonElement> = {
target: T,
type: string
}
type MyEvent<T extends HTMLElement = HTMLButtonElement> = {
target: T,
type: string
}
let myEvent: MyEvent = {
target: document.createElement('button'),
type: 'button'
}
// 有默认类型的泛型要放在没有默认类型的泛型后面
type MyEvent2<T extends string, R extends HTMLElement = HTMLElement> = {
target: R,
type: T
}
4.3 类型驱动开发
类型驱动开发,先草拟类型签名,然后填充值的编程风格。
编程TypeScript程序时,先定义函数的类型签名,即“受类型的指引”,然后再具体实现。
第5章 类和接口
5.1 类和继承
访问修饰符会自动把参数赋值给this。readonly
在初始化赋值后,这个属性只能读取,不能再赋其他值。
访问修饰符:
-
public
任何地方都可访问。这是默认的访问级别。
-
protected
可由当前类及其子类的实例访问。
-
private
只可由当前类的实例访问。
abstract
关键字表明,不能直接初始化该类。
type Color = 'Black' | 'White'
type Files = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H'
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
class Position {
constructor(private file: Files, private rank: Rank) { }
distanceFrom(postion: Position) {
return {
rank: Math.abs(postion.rank - this.rank),
file: Math.abs(postion.file.charCodeAt(0) - this.file.charCodeAt(0))
}
}
}
// 表明该类不能直接实例化。
abstract class Piece {
protected position: Position
constructor(private readonly color: Color,
file: Files, rank: Rank) {
this.position = new Position(file, rank)
}
// 如果子类愿意,也可以覆盖默认实现。默认修饰符为public
moveTo(position: Position) {
this.position = position
}
// 子类必须实现一个名为canMoveTo的方法,而且要兼容指定的签名。
abstract canMoveTo(position: Position): boolean
}
// 创建一个King类继承Piece
class King extends Piece {
canMoveTo(position: Position) {
let distance = this.position.distanceFrom(position)
return distance.rank < 2 && distance.file < 2
}
}
// 开始新游戏
class Game {
private pieces = Game.makePieces()
private static makePieces() {
new King('White', 'E', 1)
new King('Black', 'C', 8)
}
}
- 类使用class关键字声明。扩展类时使用extends关键字。
- 类可以是具体的,也可以是抽象的(abstract)。抽象类可以有抽象方法和抽象属性。
- 方法的可见性可以是private、protected和public(默认)。方法分为实例方法和静态方法两种。
- 类可以有实例属性,可见性也可以是private、protected和public(默认)。实例属性可在构造方法的参数中声明,也可以通过属性初始化语句声明。
- 声明实例属性时可以使用readonly把属性标记为只读。
5.2 super
super有两种调用方式:
- 方法调用,例如super.take。
- 构造方法调用。使用特殊的形式super(),而且只能在构造方法中调用。如果子类有构造方法,在子类的构造方法中必须调用super(),把父子关系连接起来。
5.3 以this为返回类型
class Set {
has(val?: number): boolean {
// ...
return true
}
add(val?: number): this {
// ...
return this
}
}
new Set().add().add().has()
5.4 接口
接口是一种命名类型的方法。类型别名和接口算是同一概念的两种句法(就像函数表达式和函数声明之间的关系)。
//类型别名方式
type Food = {
calories: number,
tasty: boolean
}
type Sushi = Food & {
salty: boolean
}
type Cake = Food & {
sweet: boolean
}
// 接口方式
interface Food {
calories: number,
tasty: boolean
}
interface Sushi extends Food {
salty: boolean
}
interface Cake extends Food {
sweet: boolean
}
类型和接口之间的区别:
-
类型别名更为通用,右边可以是任何类型,包括类型表达式(类型、&或|等类型运算符);而接口声明右边必须为结构。
-
扩展接口时,TypeScript将检查扩展的接口是否可赋值给被扩展的接口。
interface A { age: number } interface A{ age: string // error }
-
同一作用域中的多个同名接口将自动合并;同一作用域中的多个同名类型别名将导致编译时错误。
5.4.1 声明合并
两个接口不能有冲突。如果声明了泛型,必须使用一样的名称和类型。
5.4.2 实现
声明类时,可以使用implements
关键字指明该类满足某个接口。
interface Person {
name: string,
age: number,
say(content: string): string
}
class Xiaoming implements Person {
constructor(public name: string, public age: number) {}
say(content: string): string {
return content
}
// 如果需要,在此基础上还可以实现其他方法和属性
run() {}
}
接口可以声明实例属性,但是不能带有可见性修饰符(private,protected和public),也不能使用static关键字。
一个类不限于只能实现一个接口,而是想实现多少都可以。
5.4.3 实现接口还是扩展抽象类
实现接口更通用、更轻量,而抽象类的作用更具体、功能更丰富。
接口是对结构建模的方式。在值层面可表示对象、数组、函数、类或类的实例。接口不生成JavaScript代码,只存在于编译时。
抽象类只能对类建模,而且生成运行时代码,即JavaScript类。抽象类可以有构造方法,可以提供默认实现,还能为属性和方法设置访问修饰符。
如果多个类公用一个实现,使用抽象类。如果需要一种轻量的方式表示“这个类是T型”,使用接口。
5.5 类是结构化类型
TypeScript是结构化类型语言。
class Zebra {
trot() {
// ...
}
}
class Poodle {
trot() {
// ...
}
}
function ambleAround(animal: Zebra) {
animal.trot()
}
ambleAround(new Zebra()) // OK
ambleAround(new Poodle()) // OK
ambleAround({ trot() { } }) // OK
5.6 类既声明值也声明类型
在TypeScript中,多数时候,表达的要么是值要么是类型。类型和值位于不同的命名空间。
类和枚举比较特殊,它们既在类型命名空间中生成类型,也在值命名空间中生成值。
class C { }
let c: C = new C
enum E { F, G }
let e: E = E.F
5.7 多态
类和接口对泛型参数也有深层支持,包括默认类型和限制。
class MyMap<K,V> {
constructor(public initialKey: K, public initialValue: V) {} // 在构造方法中不能声明泛型。应该在类声明中声明泛型。
get(key: K): V {
return this.initialValue
}
merge<K1, V1>(map: MyMap<K1, V1>): MyMap<K | K1, V | V1> { // 实例方法可以访问类一级的泛型,也可以自己声明泛型。
// ...
}
static of<K, V>(k: K, v: V): MyMap<K, V> { // 静态方法不能访问类的泛型,就像值层面不能访问类的实例变量一样。
// ...
}
}
interface MyMap<K, V> {
get(key: K): V
set(key: K, value: V): void
}
// 可以显式为泛型绑定具体类型,也可以让TypeScript自动推导。
let a = new MyMap<string, number>('k', 1)
let b = new MyMap('v', 2)
5.8 混入
混入就是把行为和属性混合到类中。混入有以下特性:
- 可以有状态(即实例属性)。
- 只能提供具体方法(与抽象方法相反)。
- 可以有构造方法,调用的顺序与混入类的顺序一致。
type ClassConstructor<T> = new (...args: any[]) => T
function withEZDegbug<C extends ClassConstructor<{ getDebugValue(): object }>>(Class: C) {
return class extends Class {
constructor(...args: any[]) {
super(...args)
}
debug() {
let Name = Class.constructor.name
let value = this.getDebugValue()
return Name + '(' + JSON.stringify(value) + ')'
}
}
}
class HardToDebugUser {
constructor(private id: number, private fristName: string, private lastName: string) { }
getDebugValue() {
return {
id: this.id,
name: this.fristName + ' ' + this.lastName
}
}
}
let User = withEZDegbug(HardToDebugUser)
let user = new User(3, 'chen', 'zilin')
console.log(user.debug());
5.9 装饰器
装饰器为类、类方法、属性和方法参数的元编程提供简洁的句法。装饰器就是在装饰目标上调用函数的一种句法。
type ClassConstructor<T> = new (...args: any[]) => T
function serializable<T extends ClassConstructor<{
getValue(): PayLoad
}>>(Constructor: T) {
return class extends Constructor {
seialize() {
return this.getValue().toString()
}
}
}
5.10 模拟final类
final关键字的作用:某些语言使用这个关键字把类标记为不可扩展,或者把方法标记为不可覆盖。
把constructor标记为private后,不能使用new运算符实例化类,也不能扩展类。
class MessageQueue {
private constructor(private message: string[]) {}
static create(message: string[]) {
return new MessageQueue(message)
}
}
class BadQueue extends MessageQueue {} // 无法扩展
new MessageQueue() // 无法直接实例化
MessageQueue.create(['string'])
5.11 设计模式
5.11.1 工厂模式
创建某种类型的对象的一种方式,这种方式把创建哪种具体对象留给创建该对象的工厂决定。
type Shoe = {
purpose: string
}
class BalletFlat implements Shoe {
purpose = 'dancing'
}
class Boot implements Shoe {
purpose = 'woodcutting'
}
class Sneaker implements Shoe {
purpose = 'walking'
}
let Shoe = {
create(type: 'balletFlat' | 'boot' | 'sneaker'): Shoe {
switch(type) {
case 'balletFlat': return new BalletFlat()
case 'boot': return new Boot()
case 'sneaker': return new Sneaker()
}
}
}
5.11.2 建造者模式
建造者模式把对象的建造方式与具体的实现方式区分开。
class RequestBuilder {
private url: string | null = null
private method: 'get' | 'post' | null = null
setURL(url: string): this {
this.url = url
return this
}
setMethod(method: 'get' | 'post'): this {
this.method = method
return this
}
}
new RequestBuilder().setMethod('get').setURL('/api')
第6章 类型进阶
6.1 类型之间的关系
6.1.1 子类型和超类型
子类型,给定两个类型A和B,假设B是A的子类型,那么在需要A的地方都可以放心使用B。
超类型,给定两个类型A和B,假设B是A的超类型,那么在需要B的地方都可以放心使用A。
- Array是Object的子类型。
- Tuple是Array的子类型。
- 所有类型都是any的子类型。
- never是所有类型的子类型。
- 如果Bird类拓展自Animal类,那么Bird是Animal的子类型。
意味着
- 需要Object的地方都可以使用Array。
- 需要Array的地方都可以使用Tuple。
- 需要any的地方都可以使用Object。
- never可在任何地方使用。
- 需要Animal的地方都可以使用Bird。
6.1.2 型变
对预期的结构,可以使用属性的类型<:预期类型的结构,但是不能传入属性的类型是预期类型的超类型的结构。
- A <: B指“A类型是B类型的子类型,或者为同种类型”。
- A >: B指“A类型是B类型的超类型,或者为同种类型”。
型变的四种方式:
-
不变
只能是T。
-
斜变
可以是<:T。
-
逆变
可以是>:T。
-
双变
可以是<:T或>:T。
函数型变
函数A是函数B的子类型需要满足以下条件:
- 函数A的this类型未指定,或者>:函数B的this类型。
- 函数A的各个参数类型>:函数B的相应类型。
- 函数A的返回类型<:函数B的返回类型。
6.1.3 可赋值性
可赋值性指在判断需要B类型的地方是否使用A类型时TypeScript采用的规则。
在满足下述任一条件时,A类型可赋值给B类型(枚举类型例外):
- A <: B。
- A是any。
6.1.4 类型拓宽
TypeScript在推导类型时会放宽要求,故意推导出一个更宽泛的类型,而不限定为某个具体的类型。
声明变量时如果允许以后修改变量的值,变量的类型将拓宽,从字面值放大到包含该字面量的基类型。
let a = 'x' // string
const b = { x: 3 } // {x: number}
如果使用let或var重新为非拓宽类型赋值,TypeScript将自动拓宽。
const a = 'x' // 'x'
let b = a // string
// 倘若不想让TypeScript拓宽,一开始声明时要显示注解类型;
const c: 'x' = 'x' // 'x'
let d = c // 'x'
// 初始化为null或undefined的变量将拓宽为any;
let a = null // any
let b = undefined // any
a = b // any
// 当初始化的null或undefined的变量离开声明时所在的作用域,TypeScript将为其分配一个具体类型
function x() {
let a = null // any
a = 3 // any
a = 'b' // any
return a
}
x() // string
const类型
// const类型可以禁止拓宽,还可以将递归成员设为readonly
let a = { x: 3 } // {x: number}
let b = { x: 3 } as const // {readonly x: 3}
let c = { x: { y: 3 } } as const // {readonly x: {readonly y: 3}}
多余属性检查
TypeScript检查一个对象是否可赋值给另一个对象类型时,也涉及到类型拓宽。
尝试把一个新鲜对象字面量类型T赋值给另一个类型U时,如果T有不在U中的属性,将报错。
新鲜对象字面量类型指的是TypeScript从对象字面量中推导出来的类型。如果对象字面量有类型断言(as),或者把对象字面量赋值给变量,那么新鲜对象字面量类型将拓宽为常规的对象类型,也就不能称为新鲜对象字面量类型。
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({
baseURL: 'https://chenzilin.com'
})
new API({
baseURL: 'https://chenzilin.com',
badTier: 123
} as Options)
let badOptions = {
baseURL: 'https://chenzilin.com',
badTier: 123
}
new API(badOptions)
6.1.5 细化
TypeScript采用的是基于流的类型推导。
JavaScript有七个假值:null、undefined、NaN、0、-0、""和false。
type UserEvent = {
value: string | [number, number]
}
function handle(event: UserEvent) {
if (typeof event.value === 'string') { // 细化
event.value // string
}
event.value // [number, number]
}
类型细化的能力有限,只能细化当前作用域中变量的类型,一旦离开这个作用域,类型细化能力不会随之转移到新作用域中。
6.2 全面性检查
全面性检查是类型检查器所做的一项检查,为的是确保所有情况都被覆盖了。
6.3 对象类型进阶
6.3.1 对象类型的类型运算符
“键入”运算符
type APIResponse = {
user: {
userId: string
friendList: {
count: number
friends: {
firstName: string
lastName: string
}[]
}
}
}
type FriendLst = APIResponse['user']['friendList'] // 键入
keyof运算符
获取对象所有键的类型,合并为一个字符串字面量类型。
type UserKeys = keyof APIResponse['user'] // "userId" | "friendList"
type FriendListKeys = keyof APIResponse['user']['friendList'] // "count" | "friends"
// 类型安全的读值函数
function get<O extends object, K extends keyof O>(o: O, k:K): O[K] {
return o[k]
}
6.3.2 Record类型
Record类型用于描述有映射关系的对象。
type Weekday = 'Mon' | 'Tue' | 'Wed'
type Day = Weekday | 'Sat' | 'Sun'
let nextData: Record<Weekday, Day> = {
Mon: 'Tue',
'Tue': 'Wed',
'Wed': 'Sat'
}
6.3.3 映射类型
Record类型也是使用映射类型实现的。
let nextDay: { [key in Weekday]: Day } = {
Mon: 'Tue',
'Tue': 'Wed',
'Wed': 'Sat'
}
// -类型运算符可以把修饰符去掉
let nextDay: { [key in Weekday]-?: Day } = {
Mon: 'Tue',
'Tue': 'Wed',
'Wed': 'Sun'
}
内置的映射类型
-
Record<Keys, Values>
键的类型为Keys、值的类型为Values的对象。
-
Partial<Object>
把Object中的每个字段都标记为可选的。
-
Required<Object>
把Object中的每个字段都标记为必须的。
-
Readonly<Object>
把Object中的每个字段都标记为只读的。
-
Pick<Object, Keys>
返回Object的子类型,只含指定的Keys。
6.3.4 伴生对象模式
把类型和对象配对在一起,称为伴生对象模式。
type Currency = {
unit: 'EUR' | 'GBP'
value: number
}
let Currency = {
DEFAULT: 'USD',
from(value: number, unit = Currency.DEFAULT): Currency {
return { unit, value }
}
}
如果一个类型和一个对象在语义上有关联,就可以使用伴生对象模式,由对象提供操作类型的实用方法,好处是可以一次性导入两者。
6.4 函数类型进阶
6.4.1 改善元组的类型推导
function tuple<T extends unknown[]>(...ts: T): T {
return ts
}
let a = [true, 1] // (number | boolean)[]
let b = tuple(true, 1) // [boolean, number]
6.4.2 用户定义的类型防护措施
// 函数细化了参数的类型,而且返回一个布尔值,可以使用类型防护措施确保类型的细化能在作用域之间转移,在使用该函数的任何地方都起作用。
function isString(a: unknown): a is string {
return typeof a === 'string'
}
6.5 条件类型
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
6.5.1 条件分配
type ToArray2<T> = T extends unknown ? T[] : T[]
type R = ToArray2<number | string> // string[] | number[]
// 在T中而不再U中的类型
type Without<T, U> = T extends U ? never : T
type O = Without<string | number | boolean, boolean> // string | number
6.5.2 infer关键字
在条件类型中声明泛型使用infer关键字。
type ElementType<T> = T extends (infer U)[] ? U : T
type G = ElementType<number[]> // number
type SecondArg<F> = F extends (a: any, b: infer B) => any ? B : never
type F = typeof Array['prototype']['slice']
type T = SecondArg<F> // number
6.5.3 内置的条件类型
-
Exclude<T, U>
计算在T中而不在U中的类型。
type A = number | string type B = string type C = Exclude<A, B> // number
-
Extract<T, U>
计算T中可赋值给U的类型。
type A = number | string type B = string type C = Extract<A, B> // string
-
NonNullable<T>
从T中排除null和undefined。
type N = { a?: number | null} type M = NonNullable<N['a']> // number
-
ReturnType<F>
计算函数的返回类型(不适用于泛型和重载的函数)。
type Fun = (a: number) => string type Re = ReturnType<Fun> // string
-
InstanceType<C>
计算类构造方法的实例类型。
type A = { new(): B } type B = { b: number } type I = InstanceType<A> // {b: number}
6.6 解决方法
6.6.1 类型断言
function formatInput(input: string) {
// ...
}
function getUserInput(): string | number {
// ...
return ''
}
let input = getUserInput()
// 优先使用as
formatInput(input as string)
// 类型断言旧句法
formatInput(<string>input)
6.6.2 非空断言
!
非空断言运算符。如果频繁使用非空断言,这就表明你的代码需要重构。
type Dialog = {
id?: string
}
function closeDialog(dialog: Dialog) {
if (!dialog.id) return
setTimeout(() => {
removeFromDom(dialog, document.getElementById(dialog.id!)!)
})
}
function removeFromDom(dialog: Dialog, element: Element) {
element.parentNode!.removeChild(element)
delete dialog.id
}
6.6.3 明确赋值断言
如果频繁使用明确赋值断言,可能表明你的代码有问题。
let userId!:string // 明确赋值,告诉TypeScript,在读取userId时,肯定已经为它赋值了。
fetchUser()
userId.toUpperCase() // OK
function fetchUser() {
userId = globalCache.get('userId')
}
6.7 模拟名义类型
// 带烙印的类型能极大地提升安全性,一般的应用用不上。
type CompanyID = string & { readonly brand: unique symbol }
type OrderID = string & { readonly brand: unique symbol }
type UserID = string & { readonly brand: unique symbol }
type ID = CompanyID | OrderID | UserID
function CompanyID(id: string) {
return id as CompanyID
}
function OrderID(id: string) {
return id as OrderID
}
function UserID(id: string) {
return id as UserID
}
function queryForUser(id: UserID) {
// ...
}
let companId = CompanyID('8a6076cf')
let orderId = OrderID('9994acc1')
let userId = UserID('d21b1bbf')
queryForUser(userId) // OK
queryForUser(companId) // Error
6.8 安全地扩展原型
interface Array<T> {
zip<U>(list: U[]): [T, U][]
}
Array.prototype.zip = function<T, U>(this: T[], list: U[]): [T, U][] {
return this.map((v, k) => tuple(v, list[k]))
}
第7章 处理错误
7.1 返回null
考虑到类型安全,返回null是处理错误最为轻量的方式。
但是返回null不利于程序的编写(不推荐)。
7.2 抛出异常
throw error
。
7.3 返回异常
function x(): T | Error { // ... }
7.4 Option类型
第8章 异步编程、并发和并行
8.1 JavaScript的事件循环
8.2 处理回调
- 使用回调可执行简单的异步任务。
- 虽然回调适合处理简单的任务,但是如果异步任务变多,很容易编程一团乱麻。
8.3 promise:让一切回到正轨
type Executor<T> = (
resolve: (result: T) => void,
reject: (error: unknown) => void
) => void
class Promise<T> {
constructor(f: Executor<T>) { }
then<U>(g: (result: T) => Promise<U>): Promise<U>
catch<U>(g: (error: E) => Promise<U>): Promise<U>
}
function readFilePromise(path: string): Promise<string> {
return new Promise((resolve, reject) => {
readFile(path, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
8.4 async和await
async function getUser() {
try {
let user = await getUserID(18)
console.info('got location', user)
} catch (error) {
console.error(error)
} finally {
console.info('done getting location')
}
}
8.5 异步流
type Events = {
ready: void
error: Error
reconnecting: { attempt: number, delay: number }
}
type RedisClient = {
on<E extends keyof Events>(event: E, f: (arg: Events[E]) => void): void
}
8.6 多线程类型安全
8.6.1 在浏览器中:使用Web职程(worker)
Promise和setTimeout等异步API是并发运行代码。
type Message = string
type ThreadID = number
type UserID = number
type Participants = UserID[]
type Commands = {
sendMessageToThread: [ThreadID, Message]
createThread: [Participants]
addUserToThread: [ThreadID, UserID]
removeUserFromThread: [ThreadID, UserID]
}
type Events = {
receivedMessage: [ThreadID, UserID, Message]
createdThread: [ThreadID, Participants]
addedUserToThread: [ThreadID, UserID]
removeUserFromThread: [ThreadID, UserID]
}
class SafeEmitter<Events extends Record<PropertyKey, unknown[]>> {
private emitter = new EventEmitter
emit<K extends keyof Events>(channel: K, ...data: Events[K]) {
return this.emitter.emit(channel, ...data)
}
on<K extends keyof Events>(channel: K, listener: (...data: Events[K]) => void) {
return this.emitter.on(channel, listener)
}
}
// 监听主线程发来的事件
let commandEmitter = new SafeEmitter<Commands>()
// 把事件发射回主线程
let eventEmitter = new SafeEmitter<Events>()
onmessage = command => {
commandEmitter.emit(command.data.type, ...command.data.data)
}
eventEmitter.on('receivedMessage', data => {
postMessage({ type: 'receivedMessage', data })
})
8.6.2 在NodeJS中:使用子进程
fork()
方法。
第9章 前后端框架
9.1 前端框架
{ "complierOptions": { "lib": ["dom", "es2015"] } }
。配置使用DOM API。
lib选项仅仅是让TypeScript在处理项目的代码时引入一组指定的类型声明,不产生额外的代码。
9.1.1 React
9.1.2 Angular 6/7
9.2 类型安全的API
可以使用由代码生成的带类型信息的API。
- 针对REST式API的Swagger。
- 针对GraphQL的Apollo。
- 针对RPC的gRPC和APache Thrift。
9.3 后端框架
对象关系器(ORM)根据数据库模式生成代码,提供的是高层API,可以执行查询、更新、删除等操作。
通过TypeScript访问数据库,建议使用ORM。
第10章 命名空间和模块
10.1 JavaScript模块简史
Dojo、YUI和LABjs => CommonJS、AMD => ES Module
10.2 import、export
在TypeScript中应该使用ES2015的import和export句法。
export let X = 3
export type X = { t: string }
10.2.1 动态导入
let locale = await import('locale_us-en')
TypeScript仅在esnext模块模式下支持动态导入。配置
{"module": "esnext"}
。
10.2.2 使用CommonJS和AMD模块
import fs from 'fs'
fs.readFile('some/file.txt')
10.2.3 模块模式与脚本模式
TypeScript采用两种模式编译TypeScript文件:模块模式和脚本模式。如果文件中有import或export语句就是模块模式,否则是脚本模式。
下述情况使用脚本模式:
- 快速验证不打算编译成任何模块系统的浏览器代码(
{"module": "none"}
),在HMTL文件中直接使用<script />
标签引入。 - 创建类型声明。
建议在使用TypeScript编写代码时始终使用模块模式。
10.3 命名空间
TypeScript提供了另一种封装代码的方式:namespace
关键字。
如果不知道使用命名空间还是模块模式,使用模块模式准没错。
namespace Network {
export function get<T>(url: string): Promise<T> {
// ...
}
export namespace HTTP {
export namespace TCP {}
}
}
namespace App {
Network.get<string>('https://chenzilin.cn/api/v1/login')
}
10.3.1 冲突
导出相同的命名空间会导致冲突。改进函数类型时对外函数声明的重载不导致冲突。
10.3.2 编译输出
命名空间不遵守tsconfig.json中的module设置。
模块优于命名空间。
10.4 声明合并
TypeScript的三种合并:
- 合并值和类型。
- 多个命名空间合并成一个。
- 多个接口合并成一个。
声明可以合并吗?
如果导入的文件没有拓展名,会依次寻找.ts、.tsx、.d.ts和.js的文件。
第11章 与JavaScript互操作
11.1 类型声明
类型声明文件的扩展名为.d.ts
。
类型声明的句法与常规的TypeScript代码类型类似,但也有几点区别:
- 类型声明只能包含类型,不能有值。这意味着,类型声明不能实现函数、类、对象或变量,参数也不能有默认值。
- 类型声明虽然不能定义值,但是可以声明JavaScript代码中定义了某个值。使用特殊的
declare
关键字(declare
可以理解为一种断言,“我发誓,我写得JavaScript代码导出了这个类型的类。”) - 类型声明只声明使用方可见的类型。如果某些代码不导出,或者是函数体内的局部变量,则不为其声明类型。
类型声明文件有以下几个作用:
- 其他人在他们的TypeScript应用中使用你提供的编译好的TypeScript代码时,TSC会寻找与生成的JavaScript文件对应的.d.ts文件,让TypeScript知道项目中涉及哪些类型。
- 支持TypeScript的代码编辑器会读取.d.ts文件,在输入代码的过程中显示有用的类型提示。
- 由于无须重新编译TypeScript代码,能极大地减少编译时间。
类型声明描述的是外参环境,与包含值的常规声明要区分开。外参变量声明使用declare
声明JavaScript文件中定义了某个变量,而常规的变量声明使用let或const关键字声明。
借助类型声明可以做到以下几件事:
- 告诉TypeScript,JavaScript文件中定义了某个全局变量。
- 定义在项目中任何地方都可以使用的全局类型,无须导入就能使用(外参类型声明)。
- 描述通过NPM安装的第三方模块(外参模块声明)。
按约定,如果有对应的.js
文件,类型声明文件使用.d.ts
扩展名;否则,使用.ts
拓展名。
在类型声明文件中,顶层值要使用declare
关键字(declare let、declare function、decare class等),而顶层类型和接口则不需要。
11.1.1 外参变量声明
外参变量声明让TypeScript知道全局变量的存在,无须显示导入即可在项目中的任何.ts或.d.ts文件中使用。
declare let process: {
env: {
NODE_ENV: 'development' | 'production'
}
}
process = {
env: {
NODE_ENV: 'development'
}
}
TSC设置: lib
lib字段可以引入内置的类型声明。
11.1.2 外参类型声明
外参类型声明保存在脚本模式下的.ts或.d.ts文件中,无须显式导入即可在项目中的其他文件里全局使用。
11.1.3 外参模块声明
把常规的类型声明放在特殊的句法declare module中:
// 'module-name'是import导入的路径
declare module 'module-name' {
export type MyType = number
export type MyDefaultType = { a: string }
export let myExport: MyType
let myDefaultType: MyDefaultType
export default myDefaultType
}
// 声明一个可被导入的模块,但是导入的类型为any
declare module 'unsafe-module-name'
// 为webpack的style-loader导入的css文件声明类型
declare module '*.css' {
let css: CSSRuleList
export default css
}
11.2 逐步从JavaScript迁移到TypeScript
11.2.1 第一步:添加TSC
{
"compilerOptions": {
"allowJs": true
}
}
设置之后,TSC就能编译JavaScript文件了。
11.2.2 第二步(上):对JavaScript代码做类型检查(可选)
{
"compilerOptions": {
"checkJs": true
}
}
设置之后,TypeScript编译JavaScript文件时会尽量推导类型,并做类型检查。
// @ts-check
,一次只检查一个JavaScript文件。// ts-nocheck
,不进行检测。
11.2.3 第二步(下):添加JSDoc注解(可选)
TypeScript能读懂JSDoc,会把JSDoc当成类型检查器的输入。
/**
*
* @param {string} word - An input string to convert
* @returns {string} The string in PascalCase
*/
export function toPascalCase(word) {
return word.replace(/\w+/g)
}
11.2.4 第三步:把文件重命名为.ts
- 根治法。
- 快速法。
11.2.5 第四步:严格要求
为所有ts文件开启TSC。
11.3 寻找JavaScript代码的类型信息
在TypeScript文件中导入JavaScript文件,TypeScript按照下述算法查找JavaScript代码的类型声明:
- 在同一级目录中寻找与
.js
文件同名的.d.ts
文件。如果存在则把该文件用作.js
文件的类型声明。 - 如果不存在这样的文件,而且allowJs和checkJs的值为true,推导
.js
文件的类型信息(由.js
文件中的JSDoc注解得出)。 - 如果无法推导,把整个模块视作
any
。
导入第三方模块JavaScript模块时,TypeScript使用的算法稍有不同:
- 在本地寻找模块的类型声明,找到就用。
- 如果在本地找不到,再分析模块的
packag.json
。如果该文件中定义了名为types
或typings
的字段,使用字段设置的.d.ts
文件做为模块的类型声明源。 - 如果没有上述字段,沿着目录结构逐级向上,寻找
node_modules/@types
文件夹,看看其中有没有模块的类型声明。 - 如果依然找不到类型声明,按照前面针对本地算法的1-3步查找。
TSC设置:types和typeRoots
typeRoots把值设为一个文件夹路径组成的数组,可以让TypeScript在这些文件夹中寻找类型声明,
"typeRoots": ["./typings", "./node_modules/@types"]
。如果想更为细致地控制,types选项指定希望TypeScript寻找哪个包的类型。
"types": ["react"]
。
11.4 使用第三方JavaScript
11.4.1 自带类型声明的JavaScript
11.4.2 DefinitelyTyped中有类型声明的JavaScript
可以在TypeSearch中搜索是否有为该模块提供外参模块声明。
11.4.3 DefinitelyTyped中没有类型声明的JavaScript
解决方案:
- 在导入的文件中加上
// @ts-ignore
指令,把该文件加入白名单。 - 把对该模块的使用加入白名单
declare module 'xxx'
。 - 自己编写外参模块声明。
- 自己编写类型声明,并且发布到NPM中。
第12章 构建和运行TypeScript
12.1.1 项目结构
源码放src目录,编译结果放dist目录。
12.1.2 构建产物
TSC生成的构建产物
文件种类 | 拓展名 | tsconfig.json标志 | 默认生成 |
---|---|---|---|
JavaScript | .js | {"emitDeclarationOnly": false} | 是 |
源码映射 | .js.map | {"sourceMap": true} | 否 |
类型声明 | .d.ts | {"declaration": true} | 否 |
声明映射 | .d.ts.map | {"declarationMap": true} | 否 |
12.1.3 设置编译目标
TSC能把多数JavaScript特性转译成旧环境支持的版本,但是没有为缺少的特性提供实现。
- target设定把代码转译成哪个JavaScript版本:es5、es2015等。
- module设定想使用的模块系统:es2015模块、commonjs模块、systemjs模块等。
- lib告诉TypeScript,目标环境支持哪些JavaScript特性:es5特性、es2015特性、dom等。
有些JavaScript特性先由TypeScript支持,而后才在某个JavaScript版本中定案,我们把这样的特性称为“ESNext”。
target
字段值:es3、es5(如不确定使用这个默认值)、es6或es2015、es2016、es2017、es2018、esnext。
lib
如果目标环境不支持较新的标准库特性,还要借助腻子脚本提供具体实现。
我们可以从流行的腻子脚本库中安装,例如core-js或者使用Babel运行通过类型检查的TypeScript代码,让@babel/polyfill自动添加腻子脚本。
12.1.5 生成源码映射
建议在开发环境中使用源码映射。
12.1.5 项目引用
如果项目中有上百个文件,那就推荐要使用项目引用。
项目引用的用法如下:
-
把一个TypeScript项目分成多个子项目。一个子项目放在一个文件夹中,该文件夹中有一个tsconfig.json文件和一些TypeScript文件。拆分项目时,应该把可能一起更新的代码放在同一个文件夹中。
-
在各个子项目的文件夹中创建一个tsconfig.json文件,写入下述内容:
{ "compilerOptions": { "composite": true, // 告诉TSC,这个文件夹是一个大型TypeScript项目的子项目。 "declareation": true, // 为这个子项目生成.d.ts声明文件。子项目可以访问各子项目的声明文件和生成的JavaScript,但是不能访问TypeScript源文件。这个行为是项目引用在重新构建大型项目时用时较少的关键。 "declarationMap": true, // 为生产的类型声明构建源码映射。 "rootDir": "." // 指明该子项目应该相对根目录(.)编译。 }, "include": [ "./**/*.ts" ], "references": [ // 在一个数组中列出该子项目依赖的其他子项目。每个引用的path字段要指向一个内有tsconfig.json文件。 { "path": "../myReferencedProject", "prepend": true } ] }
-
在根文件夹中创建一个tsconfig.json文件,引用没有被其他子项目引用的子项目。
-
使用TSC编译项目时,通过build标志让TSC把项目引用考虑进来:
tsc --build # 或者简写为tsc -b
使用extends减少tsconfig.json中的样板代码量
使用extends选项扩展:
{ "extend": "../tsconfig.base", "include": [ "./**/*.ts" ], "references": [...] }
12.1.6 监控错误
若想报告和整理运行时异常,可以使用Sentry和Bugsnag等错误监控工具。
12.2 在服务器中运行TypeScript
若想在NodeJS环境中运行TypeScript代码,把代码编译成ES2015 JavaScript,并在tsconfig.json文件中,把module字段设为commonjs:
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs"
}
}
ES2015中的import和export调用将分别被编译成require和module.exports,无须进一步打包就能在NodeJS中运行。
如果想使用源码映射,source-map-support包可以把源码映射提供给NodeJS进程。
12.3 在浏览器中运行TypeScript
如果发布一个库,供他人使用,应该始终使用umd,以便最大程度上兼容各人在自己的项目中使用的不同的模块打包工具。
- 如果使用webpack或rollup,把module设为
es2015
或更高的版本。 - 如果代码中用到了动态导入,把module设为
esnext
。 - 如果构建供其他项目使用的库,而且经过tsc处理之后没有再通过额外的构建步骤处理,为了最大程度上兼容各种加载器,把module设为
umd
。 - 如果使用CommonJS打包工具,把module设为
commonjs
。 - 如果打算使用RequireJS或其他AMD模块加载器加载代码,把module设为
amd
。 - 如果希望导出的顶级代码可在window对象上全局使用,把module设为
none
。
拥有TypeScript插件的构建工具:
- Webpack的ts-loader。
- Browserify的tsify。
- Babel的@babel/preset-typescript。
- Gulp的gulp-typescript。
- Grunt的grunt-ts。
12.4 把TypeScript代码发布到NPM中
如果是编译成供他人使用的JavaScript,有几点最佳实践要牢记于心:
- 生成源码映射,以便调试自己的代码。
- 编译为ES5,让其他人能轻易构建并运行你的代码。
- 审慎选择编译为哪种模块格式(UMD、CommonJS、ES2015等)。
- 生成类型声明,让其他TypeScript用户知晓你的代码中有哪些类型。
.npmignore 忽略上次到npm的文件。
12.5 三斜线指令
这种指令是格式特殊的TypeScript注释,作用是为TSC下达命令。
附录A 类型运算符
类型运算符 | 句法 | 适用于 |
---|---|---|
类型查询 | typeof、instanceof | 任何类型 |
键 | keyof | 对象类型 |
属性查找 | O[K] | 对象类型 |
映射类型 | [K in O] | 对象类型 |
加修饰符 | + | 对象类型 |
减修饰符 | - | 对象类型 |
只读修饰符 | readonly | 对象类型、数组类型、元组类型 |
可选修饰符 | ? | 对象类型、元组类型、函数的参数类型 |
条件类型 | ? | 泛型、类型别名、函数的参数类型 |
非空断言 | ! | 可能为空的类型 |
泛型参数的默认值 | = | 泛型 |
类型断言 | as、<> | 如何类型 |
类型防护 | is | 函数的返回类型 |
附录B 实用类型
附录C 限定作用范围的声明
关键字 | 生成类型? | 生成值? |
---|---|---|
class | 是 | 是 |
const、let、var | 否 | 是 |
enum | 是 | 是 |
function | 否 | 是 |
interface | 是 | 否 |
namespace | 否 | 是 |
type | 是 | 否 |
附录D 为第三方JavaScript模块编写声明文件的技巧
TypeScript声明及相应的类型声明
.ts | .d.ts |
---|---|
var a = 1 | declare var a: number |
let a = 1 | declare let a: number |
const a = 1 | declare const a: 1 |
function a(b) { return b.toFixed() } | declare function a(b:number): string |
class A{ b() { return 3 } } | declare class A { b(): number } |
namespace A {} | declare namespace A {} |
type A = number | type A = number |
Interface A { b?: string } | Interface A { b?: string } |
D.1 导出的类型
D.1.1全局导出
在变量、函数和类声明前面加上declare。
// 全局变量
declare let someGlobal: GlobalType
// 全局类
declare class GlobalClass {}
// 全局类型声明
type GlobalType = number
D.1.2 ES2015导出
把declare替换为export即可:
// 默认导出
export default defaultExport
// 具名导出
export class SomeExport {
a: SomeOtherType
}
// 类型导出
export type SomeType = {
a: number
}
D.1.3 CommonJS导出
第三方CommonJS模块的类型声明中只能有一个导出语句。若想导出更多东西,要利用声明合并行为。
D.1.4 UMD导出
UMD模块与ES2015基本一样,唯一区别是若想让模块在脚本模式下的文件中全局可用,要使用特殊的export as namespace
句法。
export as namespace MyModule
。