June 19, 2019

ts学习笔记

这篇文章主要是读 ts 入门教程ts 中文网 所记下来的一些笔记,作为后面学习的一个参考点;看完文章后,对知识点做一定的提取;但是描述起来比较简单,主要针对部分关键知识点;如果需要系统的学习的话,就需要把教程看了。

类型声明

  • 声明基础数据带有类型:
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
29
30
31
// 声明数字
let num: number

num = 1
num = 'str' // error

// 声明字符串
let str: string = 'str'

// 声明多种类型
let value: number | string

value = 1
value = 'str'


// 声明任意数据类型;若声明函数没有指明类型,也是 any 类型
let value: any

value = true
value = { foo: 'foo' }

// 声明函数返回的类型
// 函数参数支持类型也是与声明变量类似
function foo (bar: string | number): boolean {
    var flag:boolean = true

    console.log(bar.length) // length 属性只在 string 类型存在,number 类型不存在;因为编译器会报错
    console.log(bar.toString()) // 两种类型都有的方法,能够正常运行,因此当声明变量为多个类型的时候,访问的属性需要两个类型都具有的方法或者属性
    return flag
}
  • interface 定义对象类型
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
29
30
31
32
33
34
35
36
37
38
39
40
interface Person {
    name: string,
    age: number
}

let person: Person = {
    name: 'kyrie',
    age: 20
}

// 当有属性是不确定添加,部分对象包含,部分对象不包含的情况,可以使用这样子来表示:
interface Person {
    name: string,
    nickName?: string,
    age: number,
    id?: number,
}

let person: Person = {
    name: 'kyrie',
    age: 20,
    id: 10
}

// 当一个对象需要一些不确定名称的值,可以使用任意属性的定义方法,但是已知的属性必须是任意属性类型的子集;例如已知属性全部都是 string 类型;那么任意属性可以定义为 string, 但不能够定义为 number;如果已知属性有number, string,那么任意属性定义为 string 也不正确
interface: Person {
    readonly id: number,
    name: string,
    age: number,
    [propName: string]: any // 所以这里可以定义为 any 类型;那么 string number 就是 any 的子集
}

// 注意上面的接口有一个是 readonly 关键词,表示只读,也就是第一次赋值,不能二次赋值

let person: Person = {
    id: 10,
    name: 'kyrie',
    age: 20
}
person.id = 11 // error
  • 定义数组类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 类型 + 方括号表示方法,类型包括 any 的定义
let arr: number[] = [1, 2, 3] // 数组元素只能是数字

// 2. 泛形(后面会说到泛型的详细信息)
let arr: Array<number> = [1, 2, 3]

// 3. 接口类型
interface NumberArray {
    [index: number]: number
}
let arr: NumberArray = [1, 2] // successful
let arr2: NumberArray = [1, 2, '3'] // error

// 4. 内置对象,常见的有:IArguments(普通函数的参数), NodeList, HTMLCollection

function foo () {
    // let args: number[] = arguments // error
    let args: IArguments = arguments

    console.log(args)
    console.log(typeof args)
}
  • 定义函数类型
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 通常函数定义分两种,一种是函数声明,另外一种是函数表达式
function foo () {} // 函数声明

let foo = function () {} // 函数表达式

// 对于普通函数声明:
function foo (id: number, name: string = 'anonymous', age?: number, address?: string) {}

// 上面这个函数表示,id 这个参数比必选的,name 参数有默认值,如果不传的值是 'anonymous', age 参数是可选的,不传就没有值了;
// 可选参数且没有默认值的参数,只能在函数声明的最后,可以有多个可选没默认值参数;有默认值的参数,后面可以接必须参数

// 剩余参数的处理;实际上剩余参数是数组,我们可以声明数组的方式来处理
function foo (id: number, name: string, ...msg: any[]) {
    console.log(msg)
}

// 对于函数表达式
let foo = function (id: number, name: string): any {
    // 这样子定义是ok的,能够通过编译
} 

// 实际上只是对右边的匿名函数进行声明类型,没有对 foo 的变量的类型声明,完整的声明方式应该是这样子的:
// 注意区分箭头函数的区别
let foo: (id: number, name: string) => any = function (id: number, name: string): any {
    // xxx
}

// 另外一种为函数表达式变量定义类型的方式可以使用接口形式
interface fooFunction {
    (id: number, name: string): boolean
}
let foo: fooFunction = function (id: number, name: string): boolean {
    return !!id
}

// 函数重载,需求:假设对传的参数是number,则扩大十倍,是字符串则添加前缀:
function calc (value: number | string): number | string {
    if (typeof value === 'number') {
        return value * 10
    } else {
        return '_' + value
    }
}

// 实际上是应该对参数是number 则返回值是 number; 参数是 string ,则返回值是 string
function calc (value: number): number;
function calc (value: string): string;
function calc (value: number | string): number | string {
    if (typeof value === 'number') {
        return value * 10
    } else {
        return '_' + value
    }
}
// 前两次声明是函数的定义,最后的声明是函数的实现;ts 会从最开始声明的进行匹配
  • 类型断言 类型断言就是 ts 允许开发者覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」。
1
2
3
4
5
6
7
8
function getLength (value: string | number): void {
    // 断言方式有两种
    // 第一种是 <type> 的方式
    console.log((<string>value).length)
    // 第二种是 值 as 类型
    console.log((value as string).length)
    // 因为第一种方式在 jsx 里面会存在误区,所以在 jsx 语法里面,只能使用第二种
}

简要解释一下上面的函数,因为接收的参数有stringnumber两种类型,而number类型是没有length属性,前面说到,需要两个类型共有的方法或者属性才能使用;而我们这里强制断言为string,因此编译的时候不会报错。

  • 类型别名

就例如上面的getLength方法,有些时候我们需要定义多个类型的时候,如果经常要重复编写就会很麻烦,我们可以自定义一个类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 type 关键词
type numStr = string | number
type numStrFun = numStr | () => string

function getLength (value: numStr) {
    // ...
}

// type 关键词除了可以定义类型之外,还可以字符串字面量
type top3 = 'first' | 'second' | 'third'
function typeFun (value: top3) {
    console.log(value)
}

typeFun('first')
typeFun('fourth') // 报错,因为参数 fourth 不在定义的字符串字面量中
  • 元组类型 通常数组是表示同一类型的元素,而元组(Tuple)则可以表示不同类型;
1
2
3
4
5
6
7
8
9
10
11
var multiType: [string, number]

multiType = ['1', 1]
// 元组与数组比较类似,可以通过下标用来赋值或者取值,但是赋值的时候,这个值需要下标对应类型一致,否则会出错
multiType[0] = 2 // error,类型对不上

// 可以使用 push 添加属性,也可以使用 slice 来获取不同值
multiType.push('2') // 注意使用 push 的时候,需要初始化值之后才能使用
multiType.push(2)
multiType.slice(0) // [ '1', 1, '2', 2 ]
multiType.push(true) // error 类型对不上

枚举

通常是用于取值在一定范围的场景,例如一周7天,颜色固定红绿蓝三个颜色

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
enum Color {
    red,
    green,
    blue
}

// 取值
Color['red'] // 0
Color[1] // 'green'

// 上面的枚举类型,转换到js之后是这样子的:
var Color;
(function (Color) {
    Color[Color["red"] = 0] = "red";
    Color[Color["green"] = 1] = "green";
    Color[Color["blue"] = 2] = "blue";
})(Color || (Color = {}));

// 执行后相当于:
var Color = {
    0: 'red',
    1: 'green',
    2: 'blue',
    'red': 0,
    'green': 1,
    'blue': 2
}
// 通过下标与字符串都能够访问到对应数据

实际上枚举的步长每次增加1;如果我们设置初始的值是1,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Color {
    red = 1,
    green,
    blue
}

Color[2] // 'green'
Color[3] // 'blue'
// 所以这个数可以是负数或者是小数,每次增加步长都是1;也可以是计算所得值:

enum Color {
    red,
    green,
    blue = 'blue'.length
}

// 需要注意的是,这个计算所得值对应的枚举项需要是最后的值;如果不是最后的值,那么计算所得值后面的枚举项将不能每次步长+1;无法获取确切的初始值而报错。
  • 类 ts 的类与 es6 的类有比较多相同的地方;增加的地方有:
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
29
30
31
32
33
34
35
36
37
38
// 1. 类的属性与方法添加修饰符:public(默认值,任何对象都能够访问到), private(只能在当前类的方法访问), protected(只能在当前类或者子类访问)
class Animal {
    public name
    private nickName

    constructor (name: string, nickName: string) {
        this.name = name
        this.nickName = nickName
    }

    // 默认是 public,实例对象可以访问
    getName () {
        return this.name
    }
    // 私有方法,只能在当前类访问
    private getNickName () {
        return this.nickName
    }
    // 保护方法,只能在当前类或者子类中访问
    protected getFullName () {
        return this.name + ':' + this.nickName
    }
}

// 2. 抽象类,使用 abstract 关键词声明的类;抽象类是不能够直接实例化,只能通过有子类继承,并由子类实现所有抽象类的抽象方法
abstract class Eat {
    abstract eatFood ()
}

class Animal {
    public food
    constructor (food: string) {
        this.food = food
    }
    eatFood () {
        console.log('eat food:', this.food)
    }
}

类与接口 (interface)

通常类大多数都是继承关系,子类通过继承父类,然后加多一些特有的方法属性等;但有时候继承类并不能实现所有方法,子类(假设为A)可能有些方法需要在别的类(假设为B类)实现;而且这个B类也为其他类别(假设为C类)提供;B类就有点公共类的意思了,同时为A,C类提供;

例如一个场景:

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

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
29
30
31
32
33
34
35
36
37
38
39
40
interface Alarm {
    alert ()
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
    alert () {
        console.log('SecurityDoor alert')
    }
}

class Car implements Alarm {
    alert () {
        console.log('Car alert') 
    }
}

// 并且一个类可以实现多个接口:
interface Alarm {
    alert()
}

interface Light {
    lightOn ()
    lightOff ()
}

class Car implements Alarm, Light {
    alert() {
        console.log('Car alert')
    }
    lightOn() {
        console.log('Car light on')
    }
    lightOff() {
        console.log('Car light off')
    }
}

上例中,Car 实现了 Alarm 和 Light 接口,既能报警,也能开关车灯

这样子在使用 class 语法的时候,能够提高类的灵活性;容易添加部分公共类方法,扩充对象

泛型

通常在定义函数、接口、类的时候,会定义一些类型;但有时候有些类型是函数执行的时候才能确定,并且是根据传入参数的类型,返回的类型也要一致;那样子就有点存在动态类型的意思了;而泛型的作用是可以在定义的时候不预先指定类型,在运行的时候再确定好数据的类型:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function repeatValue<T> (value: T): T[] {
    var arr: T[] = []

    for (var i = 0; i < 3; i++) {
        arr.push(value)
    }

    return arr
}

repeatValue('str') // ['str', 'str', 'str']
repeatValue(1) // [1, 1, 1]

// 泛型中的类型可以是 interface 的类型,也可以是基础的数据类型
// 可以在函数中定义多个泛型的类型
function reverse<T, U> (v1: T, v2: U): [U, T] {
    return [v2, v1]
}
reverse('1', 2) // [2, '1']

// 由于泛型定义的变量并不知道是什么类型,因此如果调用变量的一些属性的时候,因为类型不确定的关系,系统会报错
// 这个泛型的默认类型为 string; 可以在函数使用的时候,传的参数没有指定确切类型的时候使用
function getLen<T = string> (value: T): number {
    return value.length // error
}

// 但是我们的泛型也是可以添加约束的,那么在约束范围内使用方法或者属性就可以了:
interface lengthInterface {
    length: number
}

function getLen<T extends lengthInterface> (value: T): number {
    return value.length
}

// 泛型接口 interface
// 没有使用泛型定义的接口,根据接口定义函数表达式的函数
interface includeFunction {
    (str: string, subStr: string): boolean    
}
let isInclude: includeFunction
isInclude = function (str: string, subString): boolean {
    return str.indexOf(subStr) > -1
}

// 使用泛型定义接口的话
interface repeatFunction {
    <T>(value: T, len: number): Array<T>
}
let repeatByLen: repeatFunction
repeatByLen = function<T> (value: T, len: number): Array<T> {
    let arr: Array<T> = []

    for (let i = 0; i < len; i++) {
        arr.push(value)
    }
    return arr
}
repeatByLen('foo', 3) // ['foo', 'foo', 'foo']

// 在类中添加泛型
class NumberClass<T> {
    value: T,
    add: (x: T, y: T) => T
}

let num = new NumberClass()
num.value = 10
num.add = function (x, y) { return x + y }