0%

《TypeScript编程》阅读

第一章 导言

TS具有类型安全(借助类型避免程序做无效的事情)。比如JS在对一些不同类型进行操作的时候,不会抛出异常,TS就会抛出。

第二章 TypeScript概述

2.1 编译器

  1. TS的特殊之处在于不直接编译成字节码,而是编译成JS代码。

  2. TS编译器生成AST之后,真正运行代码之前,会对代码做类型检查。

    1-3步由TSC操作,4-6步由浏览器、NodeJS或者其他JS引擎的JS运行时操作。

2.2 类型系统

  1. 类型系统:类型检查器为程序分配类型时使用的一系列规则。有两种:一种通过显式句法告诉编译器所有值的类型,另一种自动推导值的类型。

  2. TS身兼两种类型系统。可以显式注解,也可以让TypeScript推导。

  3. 告诉TS类型,使用注解。value:type。

    1
    2
    3
    let a:number = 1
    let b:string = 'hello'
    let c:boolean[] = [true, false]
  4. 如果想让TS推导,就去掉注解。TS自动推导。

    1
    2
    3
    let a = 1
    let b = 'hello'
    let c = [true, false]
  5. TS vs. JS

    类型是如何绑定的?

    JS动态绑定类型,必须运行程序才能知道类型,运行前对类型一无所知。TS是渐进式类型语言,编译时知道所有类型能让TS充分发挥作用,但是在编译之前不需要知道全部类型。

    是否自动转换类型?

    JS是弱类型语言,如果执行无效操作,JS会根据规则判断真实意图。3+[1]结果是”31”。TS遇到这种运算会报错。

    何时检查类型?

    JS不在乎使用的是什么类型,会尽所能把值转换成预期类型。TS在编译时会对代码做类型检查。

    何时报告错误?

    JS运行时抛出异常或执行隐式类型转换,意味着必须真正运行程序才知道有些操作无效。TS编译时报告句法或者类型相关错误,会在输入代码后立即反馈。

    堆栈溢出、网络断连和恶意的用户输入在编译时无法捕获。

2.3 代码编辑器设置

2.3.1 tsconfig.json

  1. 每个TS项目应该在根目录放一个tsconfig.json文件。定义要编译哪些文件、把文件编译到哪个目录中,使用哪个版本的JS运行。

  2. 内容:

2.3.2 tslint.json

  1. 保存TSLint配置,为代码制订风格上的约定。

  2. 内容:

2.4 index.ts

  1. 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
  2. 推导出d也是number类型。

第三章 类型全解

  1. 类型:一系列值及可以对其执行的操作。

  2. 类型检查器可以通过使用的类型和具体用法判断操作是否有效。

  3. 类型:

3.1 类型术语

  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

  1. boolean值有true和false,可以比较和否定。

  2. 类型字面量:仅表示一个值的类型。

3.2.4 number

  1. 包括所有数字:整数、浮点数、正数、负数、Infinity、NaN等。

  2. 没有特殊原因,不要把值的类型显式注解为number。

3.2.5 bigint

  1. 处理较大的整数时,不用担心舍入误差。

  2. number类型最大2^53,bigint类型表示的更大。

  3. 声明时数字后面要加n。

3.2.6 string

  1. 包含所有字符串以及对字符串执行的操作。

3.2.7 symbol

  1. symbol经常用于代替对象和映射的字符串键。

  2. 这个符号是唯一的,不与其他符号相等。

3.2.8 对象

  1. 表示对象的结构。

  2. 结构化类型:只关心对象有哪些属性,不管属性使用什么名称(名义化类型)。

  3. 声明对象类型的方式,对象字面量句法。

    1
    2
    3
    4
    5
    6
    7
    8
    let a = {
    b: 'x'
    }
    let b = {
    c: {
    d: 'f'
        }
    }
    1
    2
    3
    let a: {b: number} = {
    b: 12
    }
    1
    2
    3
    const a: {b: number} = {
    b: 12
    }
  4. 对象字面量句法的意思是:这个对象的结构是这样的。

    这个对象可能是一个对象字面量,也可能是一个类:

    1
    2
    3
    4
    5
    6
    7
    let c: {
    firstName: string
    lastName: string
    } = {
    firstName: 'john',
    lastName: 'barrowman'
    }
    1
    2
    3
    4
    5
    6
    7
    class Person {
    constructor(
    public firstName: string,
    public lastName: string
    ){}
    }
    c = new Person('matt', 'smith')
  5. 添加额外的属性或者缺少必要的属性会报错:

    1
    2
    3
    4
    5
    6
    let a: {b: number}
    a = {} // 报错
    a = {
    b: 1,
    c: 2 // 报错
    }
  6. 明确赋值:先声明变量再使用值初始化的情况。

  7. 某个属性是可选的:

    1
    2
    3
    4
    5
    let a: {
    b: number
    c?: string
    [key: number]: boolean
    }

    a有个类型为number的属性b。

    a可能有个类型为string的属性c。如果有,值可以为undefined。

    a可以有多个数字属性,其值为布尔值。

    1
    2
    3
    4
    5
    6
    7
    a = {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
  8. 索引签名:告诉TS,指定的对象可能有更多的键。在这个对象中,类型为T的键对应的值为U类型。

    [key: T]。键的类型(T)必须可赋值给number或string。

    索引签名中键的名称可以是任何词,不一定非得key:

    1
    2
    3
    4
    5
    6
    let airplaneSeatingAssignments: {
    [seatNumber: string]: string
    } = {
    '34D': 'Boris Cherny',
    '34E': 'Bill Gates'
    }
  9. 可以用readonly修饰符把字段标记为只读:

    1
    2
    3
    4
    5
    let user: {
    readonly firstName: string
    } = {
    firstName: 'abby'
    }
  10. 空对象类型,{}。除了null和undefined之外的任何类型都可以赋值给空对象类型。避免使用。

  11. 声明Object,和声明{}作用基本一样。避免使用。

  12. 是否有效的对象:

3.2.9 类型别名、并集和交集

  1. 类型别名:使用变量声明(let、const、var)为值声明别名。

    1
    2
    3
    4
    5
    type Age = number
    type Person = {
    name: string
    age: Age
    }

    TS无法推导类型别名,因此必须显式注解:

    1
    2
    3
    4
    5
    let age: Age = 55 // 因为Age是number的别名,所以: Age也可以不写
    let driver: Person = {
    name: 'James May'
    age: age
    }

    同一类型不能声明两次。

    类型别名采用块级作用域,内部的类型别名将遮盖外部的类型别名。

  2. 并集类型和交集类型:并集使用|,交集使用&。

    并集通常比交集更符合常理。

3.2.10 数组

  1. 数组支持拼接、推入、搜索和切片等操作。

  2. 数组一般情况下应该保持同质。

  3. 例子:

  4. 使用const声明数组不会导致TypeScript推导出范围更窄的类型。

  5. 初始化空数组时,TS并不知道数组中元素的类型,推导类型是any。向数组中添加元素后,TS开始拼凑数组类型。

3.2.11 元组

  1. 元组是array的子类型,是定义数组的一种特殊方式,长度固定,各索引上的值具有固定的已知类型。

    1
    2
    3
    let a: [number] = [1]
    let b: [string, string, number] = ['malcolm', 'gladwell', 1963]
    b = ['queen', 'elizabeth', 'ii', 1926]
  2. 元组中?表示可选。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let trainFares: [number, number?][] = [
    [3.75],
    [8.25,7.70],
    [10.50]
    ]
    // 等价于
    let moreTrainFares: ([number] | [number, number])[] = [
    // ...
    ]
  3. 元组也支持剩余元素,即为元组定义最小长度:

    1
    2
    3
    4
    // 字符串列表,至少有一个元素
    let friends: [string, ...string[]] = ['Sara', 'Tali', 'Chloe', 'Claire']
    // 元素类型不同的列表
    let list: [number, boolean, ...string[]] = [1, false, 'a', 'b', 'c']
  4. 只读数组和元组:

    常规的数组是可变的。可以加readonly注解,想更改只读数组,使用非变型方法,例如.concat和.slice,不能使用可变型方法,例如.push和.splice。

    声明只读数组和元组,也可以使用长格式句法:

    1
    2
    3
    4
    5
    type 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

  1. null和undefined表示缺少什么。这两种类型只有它们自身一个值。undefined表示未定义,null表示缺少值。

  2. void是函数没有显式返回任何值时的返回类型,never是函数根本不返回(函数抛出异常,或者永远运行下去)时使用的类型。

  3. 例子:

    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. 枚举分为两种:字符串到字符串之间的映射和字符串到数字之间的映射。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum Language {
    English,
    Spanish,
    Russian
    }
    // 经过TS推导后上述示例得到的结果
    enum Language {
    English = 0,
    Spanish = 1,
    Russian = 2
    }
  3. 枚举中的值使用点号或者方括号表示法访问

    1
    2
    let myFirstLanguage = Language.Russian
    let mySecondLanguage = Language['English']
  4. 不必为所有成员都赋值,TS会自己推导

    1
    2
    3
    4
    5
    enum Language {
    English = 100,
    Spanish = 200 + 300,
    Russian // 推导501
    }
  5. 枚举的值也可以是字符串,甚至混用字符串和数字。

  6. 即可以通过值访问枚举,也可以通过键访问,不过通过键极易导致问题。

  7. const enum不允许方向查找。

  8. 枚举有安全问题,建议不适用。

3.3 小结

  1. 总结:

3.4 练习题

  1. 下列各值,TS推导出的类型是什么?

    1
    2
    3
    4
    5
    6
    7
    8
    let 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
  2. 为什么会报错?

    1
    2
    let 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
2
3
4
5
6
7
8
9
10
11
12
## 第四章 函数

### 4.1 声明和调用函数

1. 在JS中,函数是一等对象,可以像对象那样使用函数,TS延续了这一传统:可以赋值给变量;可以作为参数传递给其他函数;可以作为函数的返回值;可以赋值给对象和原型;可以赋予属性;可以读取属性。

2. 通常会显式注解函数的参数。因为TS多数情况下不能推导出参数的类型。返回类型能推导出来,也可以显式注解。

```typescript
function add(a: number, b: number): number {
return a + b
}
  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')

    遵守的注解规则也一样:注解参数的类型,返回类型不必须注解。

  2. 形参:声明函数时指定的运行函数所需的数据。

    实参:调用函数时传给函数的数据。

  3. 调用函数时,无需提供任何额外信息,直接传入实参,TS将会检查是否与函数形参类型兼容。如果忘记传入某个参数,或者传入的参数类型有误,TS将指出问题。

第四章 函数

4.1 声明和调用函数

4.1.1 可选和默认的参数

  1. 可以使用?把参数标记为可选的。

    1
    2
    3
    4
    5
    6
    function 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')
  2. 也可以为可选参数提供默认值,调用时无需传入参数的值。区别是带默认值的参数不要求放在参数列表的末尾,可选参数必须放在末尾。

    1
    2
    3
    4
    5
    6
    function 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足够智能,能够根据默认值推导出参数的类型。

  3. 如果愿意也可以显式注解默认参数的类型,像没有默认值的参数一样:

    1
    2
    3
    4
    5
    6
    7
    8
    type 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. 如果一个函数接受一组参数,简单起见,可以通过一个数组传入这些参数。

    1
    2
    3
    4
    function sum(numbers: number[]): number {
    return numbers.reduce((total, n) => total + n, 0)
    }
    sum([1, 2, 3]) // 结果为6
  2. 可变参数函数,即参数数量不定,而不是让参数数量固定。以前通过JS的arguments对象实现。

    JS在运行时自动在函数内定义该对象,并把传给函数的参数列表赋予该对象。但是使用arguments的时候问题是不安全。

  3. 为了确保函数可以安全接受任意个参数,应该使用剩余参数。

    1
    2
    3
    4
    function sumVariadicSafe(...numbers: number[]): number {
    return numbers.reduce((total, n) => total + n, 0)
    }
    sumVariadicSafe(1, 2, 3) // 求值结果为6
  4. 一个函数最多只能有一个剩余参数,而且必须位于参数列表的最后。

4.1.3 call、apply和bind

  1. 调用函数还可以用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)()
  2. apply为函数内部的this绑定一个值,然后展开第二个参数,作为函数传给要调用的函数。

  3. call的用法类似,不过是按顺序应用参数的,而不做展开。

  4. bind()差不多,也是为函数的this和参数绑定值。但是bind不调用函数,而是返回一个新函数,通过()、.call、.apply调用,可以再传入参数,绑定到尚未绑定值的参数上。

4.1.4 注解this的类型

  1. JS中的每个函数都有this变量,而不局限于类中的方法。以不同的方式调用函数,this的值也不同,这极易导致代码脆弱、难以理解。

  2. 一般来说,this的值为调用方法时位于点号左侧的对象。

    1
    2
    3
    4
    5
    6
    let x = {
    a() {
    return this
    }
    }
    x.a() // 在a()的定义体中,this的值为x对象
  3. 如果调用a之前重新赋值了,结果将发生变化。

    1
    2
    let a = x.a
    a() // 现在,在a()的定义体中,this的值为undefined
  4. 格式化日期的实用函数:

    1
    2
    3
    function fancyDate() {
    return ${this.getDate()}/${this.getMonth()}/${this.getFullYear()}
    }

    调用fancyDate时,要为this绑定一个Date对象,未绑定则会出现运行时错误:

    1
    fancyDate.call(new Date)
  5. 在TS中,如果函数使用this,需要在函数的第一个参数中声明this的类型。this不是常规的参数,是保留字,是函数签名的一部分:

    1
    2
    3
    function fancyDate(this: Date) {
    return ${this.getDate()}/${this.getMonth()}/${this.getFullYear()}
    }

4.1.5 生成器函数

  1. 生成器函数:简称生成器,是生成一系列值的便利方式。生成器的使用方可以精确控制生成什么值。生成器是惰性的,只有使用方要求时才会计算下一个值,可以利用生成器实现一些其他方式难以实现的操作,例如生成无穷列表。

  2. 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function* 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。

  3. TS能通过产出值的类型推导出迭代器的类型。

  4. 也可以显式注解生成器,把产出值的类型放在IterableIterator中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* createNumbers(): IterableIterator<number> {
    let n = 0
    while(1) {
    yield n++
    }
    }
    let numbers = createNumbers()
    numbers.next()
    numbers.next()
    numbers.next()
    // 求值结果依次

4.1.6 迭代器

  1. 迭代器是生成器的相对面:生成器是生成一系列值的方式,迭代器是使用这些值的方式。

  2. 可迭代的对象:有Symbol.iterator属性的对象,而且该属性的值为一个函数,返回一个迭代器。

  3. 迭代器:定义有next方法的对象,该方法返回一个具有value和done属性的对象。

  4. 创建生成器,例如调用createFibonacciGenerator,得到的值既是可迭代对象,也是迭代器,称为可迭代的迭代器,因为该值既有Symbol.iterator属性,也有next方法。

  5. 可以自己动手定义迭代器或者可迭代对象,只需分别创建实现Symbol.iterator属性和next方法的对象或类。

    定义了一个返回数字1~10的迭代器:

    1
    2
    3
    4
    5
    6
    7
    let numbers = {
    *[Symbol.iterator]() {
    for(let n = 1; n <= 10; n++){
    yield n
            }
        }
    }

    numbers是一个迭代器,调用生成器函数numbers[Symbol.iterator] ()返回一个可迭代的迭代器。

  6. 还可以使用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 调用签名

  1. 函数自身的类型的Function,也可以表示所有函数,但是并不能体现函数的具体类型。

  2. (a: number, b: number) => number
    
    1
    2
    3
    4
    5
    6
    7

    表示如下函数的类型:

    ```typescript
    function sum(a: number, b: number): number {
    return a + b
    }
  3. 函数的调用签名只包含类型层面的代码,即只有类型,没有值。因此,函数的调用签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型,但是无法表示默认值。

  4. 调用签名没有函数的定义体,无法推导出返回类型,所以必须显式注解。

  5. 函数的调用签名和具体实现十分相似。

  6. 使用签名重写函数Log:

    声明一个函数表达式Log,显式注解其类型为Log。

    不必再次注解参数的类型,因为在定义Log类型时已经注解了message的类型为string。这里不用再次注解,TS能从Log中推导出来。

    为userId设置一个默认值。userId的类型可以从Log的签名中获取,但是默认值却不得而知,因为Log是类型,不包含值。

    无需再次注解返回类型,因为在Log类型中已经声明为void。

4.1.8 上下文类型推导

  1. 上面的函数因为已经把log的类型声明为Log,所以TS能从上下文中推导出message的类型为string。这是TS类型推导的一个强大特性,称为上下文类型推导。

  2. 使用上下文类型推导的情形:回调函数。

    函数times,调用n次回调f,每次把当前索引传给f:

    1
    2
    3
    4
    5
    6
    7
    8
    function 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)
  3. 如果f不是在行内声明的,TS则无法推导出它的类型:

    1
    2
    3
    4
    function f(n) {
    console.log(n)
    }
    times(f, 4)

4.1.9 函数类型重载

  1. 前一节的type Fn = (…) => …,其实是简写型调用签名,完整形式,以Log为例:

    1
    2
    3
    4
    5
    6
    // 简写型
    type Log = (message: string, userId?: string) => void
    // 完整型调用签名
    type Log = {
    (message: string, userId?: string): void
    }

    这两种写法完全等效,只是使用的句法不同。

  2. 更复杂的函数,使用完整的签名更有好处。

  3. 重载函数:有多个调用签名的函数。

  4. JS是一门动态语言,需要以多种方式调用一个函数的方法。有时输出类型取决于输入的参数类型。

  5. TS也支持动态重载函数声明,而且函数的输出类型取决于输入类型,都得益于TS的静态类型系统。

  6. reserve函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type 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
    11
    let reserve: Reserve = (
    from: Date,
    toOrDestination: Date | string,
    destination?: string
    ) => {
    if (toOrDestination instanceof Date && destination !== undefined) {
    // 预定单程旅行
    } else if (typeof toOrDestination === 'string') {
    // 预定往返旅行
    }
    }
  7. 浏览器的DOM API中有大量重载,比如createElement用于新建HTML元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type CreateElement = {
    (tag: 'a'): HTMLAnchorElement
    (tag: 'canvas'): HTMLCanvasElement
    (tag: 'table'): HTMLTableElement
    (tag: string): HTMLElement
    }
    let createElement: CreateElement = (tag: string):HTMLElement => {
    // ...
    }

4.2 多态

  1. 使用具体类型的前提是明确知道需要什么类型,并且确认传入的确实是那个类型。但是,有时事先并不知道需要什么类型,不想限制函数只能接受某个类型。

  2. 比如实现一个filter函数。在尝试访问对象数组中某个对象属性时,TS抛出错误,毕竟没有指明对象具体结构。

  3. 泛型参数:在类型层面施加约束的占位类型,也称多态类型参数。

  4. T就像一个占位类型,根据上下文填充具体的类型。T把Filter的类型参数化了,因此才称其为泛型参数。

  5. 泛型参数使用尖括号<>声明,尖括号的位置限定泛型的作用域。

  6. TS将在调用filter函数时为泛型T绑定具体类型。为T绑定哪一个具体类型,取决于调用filter函数时传入的参数。

  7. 比如filter函数,每次调用都要重新绑定T:

  8. 泛型T把T所在位置的类型约束为T绑定的类型。

4.2.1 什么时候绑定泛型

  1. 声明泛型的位置不仅限定泛型的作用域,还决定TS什么时候为泛型绑定具体的类型。

    1
    2
    3
    4
    5
    type Filter = {
    <T>(array: T[], f: (item: T) => boolean): T[]
    }
    let filter: Filter = (array, f) =>
    // ...
  2. TS在使用泛型时为泛型绑定具体类型:对函数来说,调用函数时;对类来说,在实例化类时;对类型别名和接口来说,在使用别名和实现接口时。

4.2.2 可以在什么地方声明泛型

  1. 只要是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绑定独立的类型。

  2. map函数:

    需要两个泛型:表示输入数组中元素类型的T,表述输出数组中元素类型的U。

4.2.3 泛型推导

  1. 也可以显式注解泛型。显式注解泛型时,要么必须所有的泛型都注解,要么都不注解。

    1
    2
    3
    4
    map <string, boolean>(
    ['a', 'b', 'c'],
    _ => _ === 'a'
    )
  2. TS将检查推导出来的每个泛型是否可赋值给显式绑定的泛型,如果不可赋值,将报错:

    1
    2
    3
    4
    map<string, number>(
    ['a', 'b', 'c'],
    _ => _ === 'a' // 报错
    )

4.2.4 泛型别名

  1. 定义一个MyEvent类型,描述DOM事件,例如click或mousedown:

    1
    2
    3
    4
    type MyEvent<T> = {
    target: T
    type: string
    }

    在类型别名中只有这一个地方可以声明泛型,即紧随类型别名的名称之后、赋值运算符(=)之前。

  2. 泛型别名也可以在函数的签名中使用。

4.2.5 受限的多态

  1. 想表达类型U至少应为T,即为U设一个上限。

  2. 约束:

    T的上限为TreeNode,即T可以是TreeNode,也可以是TreeNode的子类型。

  3. 需要多个类型约束,扩展多个约束的交集&。

  4. 使用受限的多态模拟变长参数。

4.2.6 泛型默认类型

  1. 可以为MyEvent的泛型参数指定一个默认类型:

    1
    2
    3
    4
    type MyEvent<T = HTMLElement> = {
    target: T
    type: string
    }
  2. 与函数的可选参数一样,有默认类型的泛型要放在没有默认类型的泛型后面。

4.3 类型驱动开发

  1. 类型驱动开发:先草拟类型签名,然后填充值的编程风格。

4.4 小结

4.5 练习题

  1. TS能够从函数的类型中推导出哪部分的类型,参数、返回值,还是二者都可以?

    TypeScript总是推断函数的返回值类型。TypeScript有时推断函数的参数类型,如果它可以从上下文推断它们(例如,如果函数是回调)。

  2. JS的arguments对象是类型安全的吗?如果不是,我们可以采取什么措施?

    arguments对象不安全。应该使用剩余参数。

    1
    2
    function f() { console.log(arguments) } // 不安全
    function f(...args: unknown[]) { console.log(args) } // 安全
  3. 想预定立即开始的旅行。更新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
    28
    type 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. 更新本章4.2.5实现的call函数(使用受限的多态模拟变长参数),让它只支持第二个参数为字符串的函数。如果传入除此以外的函数,在编译时报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 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[]
  5. 实现一个类型安全的小型断言库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]) // false
    1
    2
    3
    function 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. 类型别名更加通用,右边可以是任何类型,包括类型表达式(类型,外加&或|等类型运算符);而在接口声明中,右边必须为结构。例如下面类型别名不能使用接口重写:

    1
    2
    type A = number
    type B = A | string
  2. 扩展接口时,ts将检查扩展的接口是否可赋值给被扩展的接口:

    1
    2
    3
    4
    5
    6
    7
    8
    interface 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将把扩展和被扩展的类型组合在一起。

  3. 同一作用域中的多个同名接口将自动合并;同一作用域中的多个同名类型别名将导致编译时错误。这个特性称为声明合并。

5.4.1 声明合并

声明合并指的是ts自动把多个同名声明组合在一起。

使用类型别名重写的话,就会报错。

两个接口不能有冲突。

如果接口中声明了泛型,那么两个接口中要使用完全相同的方式声明泛型。

5.4.2 实现

声明类时,可以使用implements关键字指明该类满足某个接口。与其他显式类型注解一样,这是为类添加类型层面约束的一种便利方式。这么做能尽量保证类在实现上的正确性,防止错误出现在下游,不知具体原因。

接口可以声明实例属性,但是不能带有可见性修饰符(private、protected和public),也不能使用static关键字。另外,像对象类型一样,可以使用readonly把实例属性标记为只读。

一个类不限于只能实现一个接口,而是想实现多少都可以。

5.4.3 实现接口还是扩展抽象类

实现接口和扩展抽象类差不多。接口更通用、更轻量,抽象类的作用更具体、功能更丰富。

接口是对结构建模的方式。接口不生成js代码,只存在于编译时。

抽象类只能对类建模,而且生成运行时代码,即js类。抽象类可以有构造方法,可以提供默认实现,还能为属性和方法设置访问修饰符。这些在接口中都做不到。

具体使用哪个取决于实际用途。如果多个类共用同一个实现,使用抽象类。如果需要一种轻量的方式表示“这个类是T型”,使用接口。