第一章 导言
TS具有类型安全(借助类型避免程序做无效的事情)。比如JS在对一些不同类型进行操作的时候,不会抛出异常,TS就会抛出。
第二章 TypeScript概述
2.1 编译器
TS的特殊之处在于不直接编译成字节码,而是编译成JS代码。
TS编译器生成AST之后,真正运行代码之前,会对代码做类型检查。
1-3步由TSC操作,4-6步由浏览器、NodeJS或者其他JS引擎的JS运行时操作。
2.2 类型系统
类型系统:类型检查器为程序分配类型时使用的一系列规则。有两种:一种通过显式句法告诉编译器所有值的类型,另一种自动推导值的类型。
TS身兼两种类型系统。可以显式注解,也可以让TypeScript推导。
告诉TS类型,使用注解。value:type。
1
2
3let a:number = 1
let b:string = 'hello'
let c:boolean[] = [true, false]如果想让TS推导,就去掉注解。TS自动推导。
1
2
3let a = 1
let b = 'hello'
let c = [true, false]TS vs. JS
类型是如何绑定的?
JS动态绑定类型,必须运行程序才能知道类型,运行前对类型一无所知。TS是渐进式类型语言,编译时知道所有类型能让TS充分发挥作用,但是在编译之前不需要知道全部类型。
是否自动转换类型?
JS是弱类型语言,如果执行无效操作,JS会根据规则判断真实意图。3+[1]结果是”31”。TS遇到这种运算会报错。
何时检查类型?
JS不在乎使用的是什么类型,会尽所能把值转换成预期类型。TS在编译时会对代码做类型检查。
何时报告错误?
JS运行时抛出异常或执行隐式类型转换,意味着必须真正运行程序才知道有些操作无效。TS编译时报告句法或者类型相关错误,会在输入代码后立即反馈。
堆栈溢出、网络断连和恶意的用户输入在编译时无法捕获。
2.3 代码编辑器设置
2.3.1 tsconfig.json
每个TS项目应该在根目录放一个tsconfig.json文件。定义要编译哪些文件、把文件编译到哪个目录中,使用哪个版本的JS运行。
内容:
2.3.2 tslint.json
保存TSLint配置,为代码制订风格上的约定。
内容:
2.4 index.ts
console.log('Hello TypeScript!')
1
2
3
4
5
6
7
8
9
10
11
### 2.5 练习题
1. ```typescript
let a = 1 + 2
let b = 2 + 3
let c = {
apple: a,
banana: b
}
let d = c.apple * 4推导出d也是number类型。
第三章 类型全解
类型:一系列值及可以对其执行的操作。
类型检查器可以通过使用的类型和具体用法判断操作是否有效。
类型:
3.1 类型术语
```typescript
function squareOf(n: number) {
return n * n
}
squareOf(2) // 求值结果为4
squareOf(‘z’) // 报错1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
### 3.2 类型浅谈
#### 3.2.1 any
1. any是在不确定类型是什么的时候使用的,不要轻易使用。
2. any相加可能会报错。
#### 3.2.2 unknown
1. 如果确实不知道一个值的类型,不要使用any,应该使用unknown。
2. ```typescript
let a: unknown = 30 // unknown
let b = a === 123 // boolean
let c = a + 10 // 错误
if (typeof a === 'number') {
let d = a + 10 // number
}TS不会把任何值推导为unknown类型,必须显式注解。
unknown类型的值可以比较。
执行操作时不能假定unknown类型为某种特定类型,必须先向TS证明一个值确实是某种类型。
3.2.3 boolean
boolean值有true和false,可以比较和否定。
类型字面量:仅表示一个值的类型。
3.2.4 number
包括所有数字:整数、浮点数、正数、负数、Infinity、NaN等。
没有特殊原因,不要把值的类型显式注解为number。
3.2.5 bigint
处理较大的整数时,不用担心舍入误差。
number类型最大2^53,bigint类型表示的更大。
声明时数字后面要加n。
3.2.6 string
- 包含所有字符串以及对字符串执行的操作。
3.2.7 symbol
symbol经常用于代替对象和映射的字符串键。
这个符号是唯一的,不与其他符号相等。
3.2.8 对象
表示对象的结构。
结构化类型:只关心对象有哪些属性,不管属性使用什么名称(名义化类型)。
声明对象类型的方式,对象字面量句法。
1
2
3
4
5
6
7
8let a = {
b: 'x'
}
let b = {
c: {
d: 'f'
}
}1
2
3let a: {b: number} = {
b: 12
}1
2
3const a: {b: number} = {
b: 12
}对象字面量句法的意思是:这个对象的结构是这样的。
这个对象可能是一个对象字面量,也可能是一个类:
1
2
3
4
5
6
7let c: {
firstName: string
lastName: string
} = {
firstName: 'john',
lastName: 'barrowman'
}1
2
3
4
5
6
7class Person {
constructor(
public firstName: string,
public lastName: string
){}
}
c = new Person('matt', 'smith')添加额外的属性或者缺少必要的属性会报错:
1
2
3
4
5
6let a: {b: number}
a = {} // 报错
a = {
b: 1,
c: 2 // 报错
}明确赋值:先声明变量再使用值初始化的情况。
某个属性是可选的:
1
2
3
4
5let a: {
b: number
c?: string
[key: number]: boolean
}a有个类型为number的属性b。
a可能有个类型为string的属性c。如果有,值可以为undefined。
a可以有多个数字属性,其值为布尔值。
1
2
3
4
5
6
7a = {b: 1}
a = {b: 1, c: undefined}
a = {b: 1, c: 'd'}
a = {b: 1, 10: true}
a = {b: 1, 10: true, 20: false}
a = {10: true} // 错误,没有属性b
a = {b: 1, 33: 'red'} // 错误,33应该是boolean而不是string索引签名:告诉TS,指定的对象可能有更多的键。在这个对象中,类型为T的键对应的值为U类型。
[key: T]。键的类型(T)必须可赋值给number或string。
索引签名中键的名称可以是任何词,不一定非得key:
1
2
3
4
5
6let airplaneSeatingAssignments: {
[seatNumber: string]: string
} = {
'34D': 'Boris Cherny',
'34E': 'Bill Gates'
}可以用readonly修饰符把字段标记为只读:
1
2
3
4
5let user: {
readonly firstName: string
} = {
firstName: 'abby'
}空对象类型,{}。除了null和undefined之外的任何类型都可以赋值给空对象类型。避免使用。
声明Object,和声明{}作用基本一样。避免使用。
是否有效的对象:
3.2.9 类型别名、并集和交集
类型别名:使用变量声明(let、const、var)为值声明别名。
1
2
3
4
5type Age = number
type Person = {
name: string
age: Age
}TS无法推导类型别名,因此必须显式注解:
1
2
3
4
5let age: Age = 55 // 因为Age是number的别名,所以: Age也可以不写
let driver: Person = {
name: 'James May'
age: age
}同一类型不能声明两次。
类型别名采用块级作用域,内部的类型别名将遮盖外部的类型别名。
并集类型和交集类型:并集使用|,交集使用&。
并集通常比交集更符合常理。
3.2.10 数组
数组支持拼接、推入、搜索和切片等操作。
数组一般情况下应该保持同质。
例子:
使用const声明数组不会导致TypeScript推导出范围更窄的类型。
初始化空数组时,TS并不知道数组中元素的类型,推导类型是any。向数组中添加元素后,TS开始拼凑数组类型。
3.2.11 元组
元组是array的子类型,是定义数组的一种特殊方式,长度固定,各索引上的值具有固定的已知类型。
1
2
3let a: [number] = [1]
let b: [string, string, number] = ['malcolm', 'gladwell', 1963]
b = ['queen', 'elizabeth', 'ii', 1926]元组中?表示可选。
1
2
3
4
5
6
7
8
9let trainFares: [number, number?][] = [
[3.75],
[8.25,7.70],
[10.50]
]
// 等价于
let moreTrainFares: ([number] | [number, number])[] = [
// ...
]元组也支持剩余元素,即为元组定义最小长度:
1
2
3
4// 字符串列表,至少有一个元素
let friends: [string, ...string[]] = ['Sara', 'Tali', 'Chloe', 'Claire']
// 元素类型不同的列表
let list: [number, boolean, ...string[]] = [1, false, 'a', 'b', 'c']只读数组和元组:
常规的数组是可变的。可以加readonly注解,想更改只读数组,使用非变型方法,例如.concat和.slice,不能使用可变型方法,例如.push和.splice。
声明只读数组和元组,也可以使用长格式句法:
1
2
3
4
5type A = readonly string[]
type B = ReadonlyArray<string>
type C = Readonly<string[]>
type D = readonly [number, string]
type E = Readonly<[number, string]>使用readonly还是,Readonly或者ReadonlyArray全凭个人喜好。
3.2.12 null、undefined、void和never
null和undefined表示缺少什么。这两种类型只有它们自身一个值。undefined表示未定义,null表示缺少值。
void是函数没有显式返回任何值时的返回类型,never是函数根本不返回(函数抛出异常,或者永远运行下去)时使用的类型。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 返回数字或者null
function a(x: number) {
if (x < 10) {
return x
}
return null
}
// 返回undefined
function b() {
return undefined
}
// 返回void
function c() {
let a = 2 + 2
let b = a * a
}
// 返回never
function d() {
throw TypeError('I always error')
}
// 返回never
function e() {
while (true) {
doSomething()
}
}
3.2.13 枚举
枚举的作用是列举类型中包含的各个值,是一种无序数据结构,把键映射到值上。
枚举分为两种:字符串到字符串之间的映射和字符串到数字之间的映射。
1
2
3
4
5
6
7
8
9
10
11enum Language {
English,
Spanish,
Russian
}
// 经过TS推导后上述示例得到的结果
enum Language {
English = 0,
Spanish = 1,
Russian = 2
}枚举中的值使用点号或者方括号表示法访问
1
2let myFirstLanguage = Language.Russian
let mySecondLanguage = Language['English']不必为所有成员都赋值,TS会自己推导
1
2
3
4
5enum Language {
English = 100,
Spanish = 200 + 300,
Russian // 推导501
}枚举的值也可以是字符串,甚至混用字符串和数字。
即可以通过值访问枚举,也可以通过键访问,不过通过键极易导致问题。
const enum不允许方向查找。
枚举有安全问题,建议不适用。
3.3 小结
总结:
3.4 练习题
下列各值,TS推导出的类型是什么?
1
2
3
4
5
6
7
8let a = 1042 // number
let b = 'apples and oranges' // string
const c = 'pineapples' // 'pineapples'
let d = [true, true, false] // boolean[]
let e = {type: 'ficus'} // {type: string}
let f = [1, false] // (number | boolean)[]
const g = [3] // number[]
let h = null // any 类型拓宽,详见6.1.4为什么会报错?
1
2let i: 3 = 3
i = 4 // 类型'4'不能赋值给类型'3'let j = [1, 2, 3]
j.push(4)
j.push(‘5’) // ‘5’不能加入number[]类型的数组中let k: never = 4 // 4不能赋值给类型’never’
let l: unknown = 4
let m = l * 2 // unknown类型不能和具体类型做运算
1 | ## 第四章 函数 |
上面使用的是具名函数句法,JS和TS还支持五种声明函数的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 具名函数
function greet(name: string) {
return 'hello' + name
}
// 函数表达式
let greet2 = function(name: string) {
return 'hello ' + name
}
// 箭头函数表达式
let greet3 = (name: string) => {
return 'hello ' + name
}
// 箭头函数表达式简写形式
let greet4 = (name: string) =>
'hello ' + name
// 函数构造方法
let greet5 = new Function('name', 'return "hello " + name')遵守的注解规则也一样:注解参数的类型,返回类型不必须注解。
形参:声明函数时指定的运行函数所需的数据。
实参:调用函数时传给函数的数据。
调用函数时,无需提供任何额外信息,直接传入实参,TS将会检查是否与函数形参类型兼容。如果忘记传入某个参数,或者传入的参数类型有误,TS将指出问题。
第四章 函数
4.1 声明和调用函数
4.1.1 可选和默认的参数
可以使用?把参数标记为可选的。
1
2
3
4
5
6function log(message: string, userId?: string) {
let time = new Date().toLocaleTimeString()
console.log(time, message, userId || 'Not signed in')
}
log('Page loaded')
log('User signed in', 'da763be')也可以为可选参数提供默认值,调用时无需传入参数的值。区别是带默认值的参数不要求放在参数列表的末尾,可选参数必须放在末尾。
1
2
3
4
5
6function log(message: string, userId = 'Not signed in') {
let time = new Date().toLocaleTimeString()
console.log(time, message, userId)
}
log('User clicked on a button', 'da763be')
log('User signed out')TS足够智能,能够根据默认值推导出参数的类型。
如果愿意也可以显式注解默认参数的类型,像没有默认值的参数一样:
1
2
3
4
5
6
7
8type Context = {
appId?: string
userId?: string
}
function log(message: string, context: Context = {}) {
let time = new Date().toISOString()
console.log(time, message, context.userId)
}
4.1.2 剩余参数
如果一个函数接受一组参数,简单起见,可以通过一个数组传入这些参数。
1
2
3
4function sum(numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0)
}
sum([1, 2, 3]) // 结果为6可变参数函数,即参数数量不定,而不是让参数数量固定。以前通过JS的arguments对象实现。
JS在运行时自动在函数内定义该对象,并把传给函数的参数列表赋予该对象。但是使用arguments的时候问题是不安全。
为了确保函数可以安全接受任意个参数,应该使用剩余参数。
1
2
3
4function sumVariadicSafe(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0)
}
sumVariadicSafe(1, 2, 3) // 求值结果为6一个函数最多只能有一个剩余参数,而且必须位于参数列表的最后。
4.1.3 call、apply和bind
调用函数还可以用call、apply和bind方法。
1
2
3
4
5// 求值结果均为30
add(10,20)
add.apply(null, [10, 20])
add.call(null, 10, 20)
add.bind(null, 10, 20)()apply为函数内部的this绑定一个值,然后展开第二个参数,作为函数传给要调用的函数。
call的用法类似,不过是按顺序应用参数的,而不做展开。
bind()差不多,也是为函数的this和参数绑定值。但是bind不调用函数,而是返回一个新函数,通过()、.call、.apply调用,可以再传入参数,绑定到尚未绑定值的参数上。
4.1.4 注解this的类型
JS中的每个函数都有this变量,而不局限于类中的方法。以不同的方式调用函数,this的值也不同,这极易导致代码脆弱、难以理解。
一般来说,this的值为调用方法时位于点号左侧的对象。
1
2
3
4
5
6let x = {
a() {
return this
}
}
x.a() // 在a()的定义体中,this的值为x对象如果调用a之前重新赋值了,结果将发生变化。
1
2let a = x.a
a() // 现在,在a()的定义体中,this的值为undefined格式化日期的实用函数:
1
2
3function fancyDate() {
return ${this.getDate()}/${this.getMonth()}/${this.getFullYear()}
}调用fancyDate时,要为this绑定一个Date对象,未绑定则会出现运行时错误:
1
fancyDate.call(new Date)
在TS中,如果函数使用this,需要在函数的第一个参数中声明this的类型。this不是常规的参数,是保留字,是函数签名的一部分:
1
2
3function fancyDate(this: Date) {
return ${this.getDate()}/${this.getMonth()}/${this.getFullYear()}
}
4.1.5 生成器函数
生成器函数:简称生成器,是生成一系列值的便利方式。生成器的使用方可以精确控制生成什么值。生成器是惰性的,只有使用方要求时才会计算下一个值,可以利用生成器实现一些其他方式难以实现的操作,例如生成无穷列表。
用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function* createFibonacciGenerator() {
let a = 0
let b = 1
while (true) {
yield a;
[a, b] = [b, a + b]
}
}
let fibonacciGenerator = createFibonacciGenerator()
fibonacciGenerator.next()
fibonacciGenerator.next()
fibonacciGenerator.next()
fibonacciGenerator.next()
fibonacciGenerator.next()
fibonacciGenerator.next()
// 求值结果依次为0,1,1,2,3,5函数名称前面的星号(*)表明这是一个生成器函数。调用生成器返回一个可迭代的迭代器。
这个生成器可一直生成值。
生成器使用yield关键字产出值。使用方让生成器提供下一个值时,例如调用next,yield把结果发给使用方,然后停止执行,只到使用方要求提供下一个值为止。因此,这里的while(true)循环不会一直运行下去,程序不会崩溃。
为了计算下一个斐波那契数,我们在一步中把b赋值给a,把a+b赋值给b。
TS能通过产出值的类型推导出迭代器的类型。
也可以显式注解生成器,把产出值的类型放在IterableIterator中:
1
2
3
4
5
6
7
8
9
10
11function* createNumbers(): IterableIterator<number> {
let n = 0
while(1) {
yield n++
}
}
let numbers = createNumbers()
numbers.next()
numbers.next()
numbers.next()
// 求值结果依次
4.1.6 迭代器
迭代器是生成器的相对面:生成器是生成一系列值的方式,迭代器是使用这些值的方式。
可迭代的对象:有Symbol.iterator属性的对象,而且该属性的值为一个函数,返回一个迭代器。
迭代器:定义有next方法的对象,该方法返回一个具有value和done属性的对象。
创建生成器,例如调用createFibonacciGenerator,得到的值既是可迭代对象,也是迭代器,称为可迭代的迭代器,因为该值既有Symbol.iterator属性,也有next方法。
可以自己动手定义迭代器或者可迭代对象,只需分别创建实现Symbol.iterator属性和next方法的对象或类。
定义了一个返回数字1~10的迭代器:
1
2
3
4
5
6
7let numbers = {
*[Symbol.iterator]() {
for(let n = 1; n <= 10; n++){
yield n
}
}
}numbers是一个迭代器,调用生成器函数numbers[Symbol.iterator] ()返回一个可迭代的迭代器。
还可以使用JS内置的常用集合类型(Array、Map、Set、String)。
1
2
3
4
5
6
7
8// 使用for-of迭代一个迭代器
for (let a of numbers) {
// 1, 2, 3, etc.
}
// 展开一个迭代器
let allNumbers = [...numbers] // number[]
// 析构一个迭代器
let [one, two, ...rest] = number // [number, number, number[]]
4.1.7 调用签名
函数自身的类型的Function,也可以表示所有函数,但是并不能体现函数的具体类型。
(a: number, b: number) => number
1
2
3
4
5
6
7
表示如下函数的类型:
```typescript
function sum(a: number, b: number): number {
return a + b
}函数的调用签名只包含类型层面的代码,即只有类型,没有值。因此,函数的调用签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型,但是无法表示默认值。
调用签名没有函数的定义体,无法推导出返回类型,所以必须显式注解。
函数的调用签名和具体实现十分相似。
使用签名重写函数Log:
声明一个函数表达式Log,显式注解其类型为Log。
不必再次注解参数的类型,因为在定义Log类型时已经注解了message的类型为string。这里不用再次注解,TS能从Log中推导出来。
为userId设置一个默认值。userId的类型可以从Log的签名中获取,但是默认值却不得而知,因为Log是类型,不包含值。
无需再次注解返回类型,因为在Log类型中已经声明为void。
4.1.8 上下文类型推导
上面的函数因为已经把log的类型声明为Log,所以TS能从上下文中推导出message的类型为string。这是TS类型推导的一个强大特性,称为上下文类型推导。
使用上下文类型推导的情形:回调函数。
函数times,调用n次回调f,每次把当前索引传给f:
1
2
3
4
5
6
7
8function times(
f: (index: number) => void,
n: number
) {
for (let i = 0; i < n; i++) {
f(i)
}
}调用times时,传给times的函数如果是在行内声明的,无需显式注解函数的类型:
1
times(n => console.log(n), 4)
如果f不是在行内声明的,TS则无法推导出它的类型:
1
2
3
4function f(n) {
console.log(n)
}
times(f, 4)
4.1.9 函数类型重载
前一节的type Fn = (…) => …,其实是简写型调用签名,完整形式,以Log为例:
1
2
3
4
5
6// 简写型
type Log = (message: string, userId?: string) => void
// 完整型调用签名
type Log = {
(message: string, userId?: string): void
}这两种写法完全等效,只是使用的句法不同。
更复杂的函数,使用完整的签名更有好处。
重载函数:有多个调用签名的函数。
JS是一门动态语言,需要以多种方式调用一个函数的方法。有时输出类型取决于输入的参数类型。
TS也支持动态重载函数声明,而且函数的输出类型取决于输入类型,都得益于TS的静态类型系统。
reserve函数:
1
2
3
4
5
6
7
8
9
10
11type Reserve = {
(from: Date, to: Date, destination: string): Reservation
(from: Date, destination: string): Reservation
} // 声明两个重载的函数签名
let reserve: Reserve = (
from: Date,
toOrDestination: Date | string,
destination?: string
) => { // 自己动手组合两个签名
// ...
}类型声明中没有组合后的签名。
实现reserve时要向TS证明检查过了调用方式:
1
2
3
4
5
6
7
8
9
10
11let reserve: Reserve = (
from: Date,
toOrDestination: Date | string,
destination?: string
) => {
if (toOrDestination instanceof Date && destination !== undefined) {
// 预定单程旅行
} else if (typeof toOrDestination === 'string') {
// 预定往返旅行
}
}浏览器的DOM API中有大量重载,比如createElement用于新建HTML元素。
1
2
3
4
5
6
7
8
9type CreateElement = {
(tag: 'a'): HTMLAnchorElement
(tag: 'canvas'): HTMLCanvasElement
(tag: 'table'): HTMLTableElement
(tag: string): HTMLElement
}
let createElement: CreateElement = (tag: string):HTMLElement => {
// ...
}
4.2 多态
使用具体类型的前提是明确知道需要什么类型,并且确认传入的确实是那个类型。但是,有时事先并不知道需要什么类型,不想限制函数只能接受某个类型。
比如实现一个filter函数。在尝试访问对象数组中某个对象属性时,TS抛出错误,毕竟没有指明对象具体结构。
泛型参数:在类型层面施加约束的占位类型,也称多态类型参数。
T就像一个占位类型,根据上下文填充具体的类型。T把Filter的类型参数化了,因此才称其为泛型参数。
泛型参数使用尖括号<>声明,尖括号的位置限定泛型的作用域。
TS将在调用filter函数时为泛型T绑定具体类型。为T绑定哪一个具体类型,取决于调用filter函数时传入的参数。
比如filter函数,每次调用都要重新绑定T:
泛型T把T所在位置的类型约束为T绑定的类型。
4.2.1 什么时候绑定泛型
声明泛型的位置不仅限定泛型的作用域,还决定TS什么时候为泛型绑定具体的类型。
1
2
3
4
5type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter = (array, f) =>
// ...TS在使用泛型时为泛型绑定具体类型:对函数来说,调用函数时;对类来说,在实例化类时;对类型别名和接口来说,在使用别名和实现接口时。
4.2.2 可以在什么地方声明泛型
只要是TS支持的调用签名的方式,都有办法在签名中加入泛型:
1:一个完整的调用签名,T的作用域在单个签名中。鉴于此,TS将在调用filter类型的函数时为签名中的T绑定具体类型。每次调用filter将为T绑定独立的类型。
2:一个完整的调用签名,T的作用域涵盖全部签名。由于T是filter类型的一部分(而不属于某个具体的签名),因此TypeScript将在声明filter类型时绑定T。
3:与1类似,不过声明的不是完整调用签名,而是简写形式。
4:与2类似,不过声明的不是完整调用签名,而是简写形式。
5:一个具名函数调用签名,T的作用域在签名中。TS将在调用filter时为T绑定具体类型,而且每次调用filter时将为T绑定独立的类型。
map函数:
需要两个泛型:表示输入数组中元素类型的T,表述输出数组中元素类型的U。
4.2.3 泛型推导
也可以显式注解泛型。显式注解泛型时,要么必须所有的泛型都注解,要么都不注解。
1
2
3
4map <string, boolean>(
['a', 'b', 'c'],
_ => _ === 'a'
)TS将检查推导出来的每个泛型是否可赋值给显式绑定的泛型,如果不可赋值,将报错:
1
2
3
4map<string, number>(
['a', 'b', 'c'],
_ => _ === 'a' // 报错
)
4.2.4 泛型别名
定义一个MyEvent类型,描述DOM事件,例如click或mousedown:
1
2
3
4type MyEvent<T> = {
target: T
type: string
}在类型别名中只有这一个地方可以声明泛型,即紧随类型别名的名称之后、赋值运算符(=)之前。
泛型别名也可以在函数的签名中使用。
4.2.5 受限的多态
想表达类型U至少应为T,即为U设一个上限。
约束:
T的上限为TreeNode,即T可以是TreeNode,也可以是TreeNode的子类型。
需要多个类型约束,扩展多个约束的交集&。
使用受限的多态模拟变长参数。
4.2.6 泛型默认类型
可以为MyEvent的泛型参数指定一个默认类型:
1
2
3
4type MyEvent<T = HTMLElement> = {
target: T
type: string
}与函数的可选参数一样,有默认类型的泛型要放在没有默认类型的泛型后面。
4.3 类型驱动开发
- 类型驱动开发:先草拟类型签名,然后填充值的编程风格。
4.4 小结
- 无
4.5 练习题
TS能够从函数的类型中推导出哪部分的类型,参数、返回值,还是二者都可以?
TypeScript总是推断函数的返回值类型。TypeScript有时推断函数的参数类型,如果它可以从上下文推断它们(例如,如果函数是回调)。
JS的arguments对象是类型安全的吗?如果不是,我们可以采取什么措施?
arguments对象不安全。应该使用剩余参数。
1
2function f() { console.log(arguments) } // 不安全
function f(...args: unknown[]) { console.log(args) } // 安全想预定立即开始的旅行。更新4.1.9节的重载的reserve函数,添加第三个调用签名。这个签名只有目的地,没有开始日期。更新reserve的实现,支持这个新增的签名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28type Reservation = unknown
type Reserve = {
(from: Date, to: Date, destination: string): Reservation
(from: Date, destination: string): Reservation
(destination: string): Reservation
}
let reserve: Reserve = (
fromOrDestination: Date | string,
toOrDestination?: Date | string,
destination?: string
) => {
if (
fromOrDestination instanceof Date &&
toOrDestination instanceof Date &&
destination !== undefined
) {
// Book a one-way trip
} else if (
fromOrDestination instanceof Date &&
typeof toOrDestination === 'string'
) {
// Book a round trip
} else if (typeof fromOrDestination === 'string') {
// Book a trip right away
}
}更新本章4.2.5实现的call函数(使用受限的多态模拟变长参数),让它只支持第二个参数为字符串的函数。如果传入除此以外的函数,在编译时报错。
1
2
3
4
5
6
7
8
9
10
11
12function call<T extends [unknown, string, ...unknown[]], R>(
f: (...args: T) => R,
...args: T
): R {
return f(...args)
}
function fill(length: number, value: string): string[] {
return Array.from({length}, () => value)
}
call(fill, 10, 'a') // string[]实现一个类型安全的小型断言库is。先草拟类型。实现之后,可以像下面这样使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Compare a string and a string
is('string', 'otherstring') // false
// Compare a boolean and a boolean
is(true, false) // false
// Compare a number and a number
is(42, 42) // true
// Comparing two different types should give a compile-time error
is(10, 'foo') // Error TS2345: Argument of type '"foo"' is not assignable
// to parameter of type 'number'.
// [Hard] I should be able to pass any number of arguments
is([1], [1, 2], [1, 2, 3]) // false1
2
3function is<T>(a: T, ...b: [T, ...T[]]): boolean {
return b.every(_ => _ === a)
}
第五章 类和接口
TS大量借鉴了C#的相关理论。
5.1 类和继承
TS类中的属性和方法支持三个访问修饰符:
public 任何地方都可访问。这是默认的访问级别。
protected 可由当前类及其子类的实例访问。
private 只可由当前类的实例访问。
访问修饰符的作用是不让类暴露过多实现细节,而是只开放规范的API,供外部使用。
总结:
类使用class关键字声明。扩展类时使用extends关键字。
类可以是具体的,也可以是抽象的(abstract)。抽象类可以有抽象方法和抽象属性。
方法的可见性可以是private、protected或public(默认)。方法分实例方法和静态方法两种。
类可以有实例属性,可见性也可以是private、protected或public(默认)。实例属性可在构造方法的参数中声明,也可通过属性初始化语句声明。
声明实例属性时可以使用readonly把属性标记为只读。
5.2 super
与JavaScript一样,TS也支持super调用。如果子类覆盖父类中定义的方法(假如Queen和Piece都实现了take方法),在子类中可以使用super调用父类中的同名方法(例如super.take)。super有两种调用方式:
方法调用,例如super.take
构造方法调用。此时使用特殊的形式super(),而且只能在构造方法中调用。如果子类有构造方法,在子类的构造方法中必须调用super(),把父子关系连接起来。
使用super只能访问父类的方法,不能访问父类的属性。
5.3 以this为返回类型
this可以用作值,此外还能用作类型。对类来说,this类型还可用于注解方法的返回类型。
使用this注解返回类型,把相关工作交给TS。
5.4 接口
类经常当做接口使用。
与类型命名相似,接口是一种命名类型的方式。
类型别名和接口是同一概念的两种句法。
类型和接口的区别:
类型别名更加通用,右边可以是任何类型,包括类型表达式(类型,外加&或|等类型运算符);而在接口声明中,右边必须为结构。例如下面类型别名不能使用接口重写:
1
2type A = number
type B = A | string扩展接口时,ts将检查扩展的接口是否可赋值给被扩展的接口:
1
2
3
4
5
6
7
8interface A {
good(x: number): string
bad(x: number): string
}
interface B extends A {
good(x: string | number): string // 错误number类型不能赋值给string
bad(x: string): string
}使用交集类型不会出现这种问题。如果把前例中的接口换成类型别名,把extends换成交集运算符(&),ts将把扩展和被扩展的类型组合在一起。
同一作用域中的多个同名接口将自动合并;同一作用域中的多个同名类型别名将导致编译时错误。这个特性称为声明合并。
5.4.1 声明合并
声明合并指的是ts自动把多个同名声明组合在一起。
使用类型别名重写的话,就会报错。
两个接口不能有冲突。
如果接口中声明了泛型,那么两个接口中要使用完全相同的方式声明泛型。
5.4.2 实现
声明类时,可以使用implements关键字指明该类满足某个接口。与其他显式类型注解一样,这是为类添加类型层面约束的一种便利方式。这么做能尽量保证类在实现上的正确性,防止错误出现在下游,不知具体原因。
接口可以声明实例属性,但是不能带有可见性修饰符(private、protected和public),也不能使用static关键字。另外,像对象类型一样,可以使用readonly把实例属性标记为只读。
一个类不限于只能实现一个接口,而是想实现多少都可以。
5.4.3 实现接口还是扩展抽象类
实现接口和扩展抽象类差不多。接口更通用、更轻量,抽象类的作用更具体、功能更丰富。
接口是对结构建模的方式。接口不生成js代码,只存在于编译时。
抽象类只能对类建模,而且生成运行时代码,即js类。抽象类可以有构造方法,可以提供默认实现,还能为属性和方法设置访问修饰符。这些在接口中都做不到。
具体使用哪个取决于实际用途。如果多个类共用同一个实现,使用抽象类。如果需要一种轻量的方式表示“这个类是T型”,使用接口。