《TypeScript编程》读书笔记

2021-07-06 fishedee 前端

0 概述

TypeScript编程读书笔记,这本书属于那种工具书,相当枯燥,但是非常全面,例子和语法很多,基本上把边边角角的都说清楚了。

2016时预计TypeScript为成为主流,而如今,连当年拥抱flow的Vue也不得不在Vue 3版本拥抱TypeScript了。不过直至如今,我才算是真正掌握了TypeScript。

TypeScript的最大特点是,它保持了js语法的动态性,同时提供了类型检查的支持,这一切都是建立在它灵活的编译时类型推导实现的,我觉得还是相当精彩巧妙的。另外,要注意的是,TypeScript是结构类型系统,不是声明类型系统(如Java,C++)

安装看这里,就不再重复了

1 基础类型

代码在这里

1.1 整数

function testNumber() {
    let a = 1 // 推导为number类型
    let b = 2.2 // 推导为number类型
    const c = 1 // 推导为number为1的类型,注意这是字面值类型,const的类型收窄
    const d: 1 = 1 // 可以在变量中指定类型
    let e: number = 12
    a += 1
    b += 2
    e += 3
    console.log(a, b, c, d, e)
}

注意有字面值类型,const默认会做类型收窄

1.2 字符串

function testString() {
    let a = 'a' // 推导为string类型
    const b = 'b' // 推导为string为b的类型,注意这是字面值类型,const的类型收窄
    const c: 'ck' = 'ck' // 可以在变量中指定类型
    let d: string = 'ge'
    a += '1'
    d += '3'
    console.log(a, b, c, d)
}

string类型

1.3 any类型

function testAny() {
    let a: any = 3 // any类型,可以执行任何的操作
    console.log(a)
    a += 3 //
    a = 'cd'
    a = a.toUpperCase()
    console.log(a)
}

any类型仅仅是为了兼容js生态做出来的,尽可能不要在项目中使用any类型

1.4 unknown类型

function testUnknown() {
    let a: unknown = 3 // unkown必须显示指定类型,ts不会推导出这个类型
    // unknown的任何操作都是错误的,因为类型不知道,以下这一句会报错
    // a.toUpperCase()

    // 加上类型检查typeof的话,ts能在控制流中分析出a是string类型
    if (typeof a === 'string') {
        a = a.toUpperCase()
    }

    console.log(a)

    // 另外一种使用unknown的方法,是使用强制类型转换,强行告诉ts这个变量就是number类型,这样做是有运行时风险的
    a = (a as number) + 3

    console.log(a)
}

unknown类型,表达了开发者暂时不知道这个类型,需要进行typeof或者instanceof检查后才能确定

1.5 bigInt类型

function testBigInt() {
    // bigint是在ES2020才发布的
    const a = 12n
    const b = 23n
    const c = a + b
    console.log(a, b, c)
}

很少会用到,兼容性也不好,对浮点也没有支持,还不如用BigDecimal的库

2 对象类型

代码在这里,这里开始就容易迷惑了,要看清楚

2.1 object类型

function testObject() {
    // object类型,可以用{},function和Array赋值
    const a: object = {
        b: 3,
    }
    const b: object = () => {
        console.log('cc')
    }
    const c: object = [1, 2, 3]

    // 但是number和string是不能赋值给object类型的,以下两句会报错
    // const d: object = 1
    // const e: object = 'kk'

    // object类型
    console.log(a, b, c)

    // object类型仅仅代表它是{},function或者Array,并不能拿到它的子段信息
    // 例如,不能拿a.b成员
    // console.log(a.b)
}

object仅仅代表它是一个对象,不能执行任何的方法,和成员访问

2.2 Object类型

function testObject2() {
    // 尽量避免使用Object与{}类型
    // Object与object是不同的,这一点真的很迷惑

    // Object类型,可以用{},function和Array赋值
    const a: Object = {
        b: 3,
    }
    const b: Object = () => {
        console.log('cc')
    }
    const c: Object = [1, 2, 3]

    // Object类型,也可以用number和string赋值
    const d: Object = 1
    const e: Object = 'kk'

    // Object类型唯一不能赋值的是null和undefined
    // const k: Object = null
    // const j: Object = undefined

    // Object类型
    console.log(a, b, c, d, e)

    // Object类型仅仅代表它是{},function或者Array,或者number,或者string,并不能拿到它的子段信息
    // 例如,不能拿a.b成员
    // console.log(a.b)
}

Object类型什么都能赋值,除了undefined,null,void和never类型

2.3 {}类型


function testObject3() {
    // Object与{}类型几乎就是一个意思

    // {}类型,可以用{},function和Array赋值
    const a: {} = {
        b: 3,
    }
    const b: {} = () => {
        console.log('cc')
    }
    const c: {} = [1, 2, 3]

    // {}类型,也可以用number和string赋值
    const d: {} = 1
    const e: {} = 'kk'

    // {}类型唯一不能赋值的是null和undefined
    // const k: {} = null
    // const j: {} = undefined

    // {}类型
    console.log(a, b, c, d, e)

    // {}类型仅仅代表它是{},function或者Array,或者number,或者string,并不能拿到它的子段信息
    // 例如,不能拿a.b成员
    // console.log(a.b)
}

迷惑大赏开始了,{}类型与Object类型是相同的意思,但是它们与object类型是不同的!object类型是不能被number和string赋值的

2.3 具体的object类型

function testConcreteObject() {
    // 声明一个a变量,它的类型是{label:string}
    const a = {
        label: '34',
    }
    console.log(a.label)

    // 声明一个b变量,它的类型是{label:string}
    // 我们也可以显式声明它的类型
    let b: { label: string } = {
        label: '34',
    }
    console.log(b)

    // c的类型是{label:string,size:number}
    const c = {
        label: '34',
        size: 10,
    }

    // ts采取结构类型系统,c变量当然可以赋值给b类型{label:string}
    // 结构类型系统不需要显式表现类型的关系,只需要类型可以满足要求即可,
    b = c

    console.log(b)

    // 声明k变量,但没有赋值
    let k: {
        label: string
    }

    // 可以赋值
    k = { label: '12' }
    k = { label: '78' }

    // 对于立即创建的变量,即使满足类型约束,也不可以赋值,因为字段的数量必须完全相同
    // 只能真的很费解FIXME
    // k = { label: '23', size: 10 }

    // 但是对于迂回创建的变量,只要满足类型约束,就可以赋值,即使字段数量更多
    const k2 = { label: '23', size: 10 }
    k = k2

    console.log(k)
}

终于到了具体的object类型,这个类型我们经常会用到。

3 类型操作符

作为一个类型推导系统,类型操作符当然是重点中的重点了

代码在这里

3.1 类型声明

function testTypeDeclarion() {
    // 声明一个类型
    type Age = number

    // 使用这个类型来声明变量
    let a: Age = 3

    console.log(a)

    // 再次声明一个类型,也是number类型
    type Height = number
    const b: Height = 4

    // Height类型的变量b可以赋值给Age类型的变量a
    // ts中依然是结构类型系统
    a = b

    console.log(b)
}

类型声明,注意ts是结构性类型系统,不同类型之间也能赋值,只要满足类型约束就可以

3.2 类型或操作

function testTypeOr() {
    // ts中类型是可以运算的
    type Age = number
    type Label = string

    // Or运算允许取两个类型的并集
    let a: Age | Label

    // 因此变量a既可以被number,也可以被string赋值
    if (Math.random() < 0.5) {
        a = 1
        console.log(a)
    } else {
        a = '2'
        console.log(a)
    }

    // 但是a由于范围太大,无法调用number或者string的方法,所以以下这一句报错
    // a.toUpperCase()
}

类型的或操作


function testTypeObjectOr() {
    // ts中类型是可以运算的
    type Dog = {
        walk(): void
        fire(): void
    }
    type Cat = {
        walk(): void
        miao(): void
    }

    let a: Dog | Cat

    // 因此变量a既可以被Dog,也可以被Cat赋值
    const rand = Math.random()
    if (rand < 0.5) {
        a = {
            walk: () => {
                console.log('walk')
            },
            fire: () => {
                console.log('fire')
            },
        }
    } else if (rand < 0.8) {
        a = {
            walk: () => {
                console.log('walk')
            },
            miao: () => {
                console.log('miao')
            },
        }
    } else {
        // a类型当然也可以是两个的组合
        a = {
            walk: () => {
                console.log('walk')
            },
            fire: () => {
                console.log('fire')
            },
            miao: () => {
                console.log('miao')
            },
        }
    }

    // 但是a由于范围太大,只能调用局部相同的方法,就是walk
    a.walk()
}

我们常用的是对象类型的或操作,对象类型的或操作让可以被赋值的类型更多,但是可以执行的行为更少

3.3 类型与操作

function testTypeAnd() {
    // ts中类型是可以运算的
    type Age = number
    type Label = string

    // Or运算允许取两个类型的并集,显然任何值都取不了
    let a: Age & Label

    // 因此变量a既可以被number,也可以被string赋值
    if (Math.random() < 0.5) {
        // 任何值都取不了,报错
        // a = 1
    } else {
        // 任何值都取不了,报错
        // a = '2'
    }

    // console.log(a as unknown)
}

类型的与操作

function testTypeObjectAnd() {
    // ts中类型是可以运算的
    type Dog = {
        walk(): void
        fire(): void
    }
    type Cat = {
        walk(): void
        miao(): void
    }

    // 是Dog与Cat的组合
    let a: Dog & Cat

    // 因此变量a只能被同时满足Dog,和Cat赋值
    // 报错
    /*
    a = {
        walk: () => {
            console.log('walk')
        },
        fire: () => {
            console.log('fire')
        },
    }
    */

    /* 也报错
        a = {
            walk: () => {
                console.log('walk')
            },
            miao: () => {
                console.log('miao')
            },
        }
    */

    // a类型只能是两个的组合
    a = {
        walk: () => {
            console.log('walk')
        },
        fire: () => {
            console.log('fire')
        },
        miao: () => {
            console.log('miao')
        },
    }

    // 但是a可以被赋值的类型更少,所以能力更大
    a.walk()
    a.fire()
    a.miao()
}

我们常用的是对象类型的与操作,对象类型的或操作让可以被赋值的类型更少,但是可以执行的行为更多

3.4 控制流类型推导

// HTMLInputElement是HTMLElement的子类
type Dog = {
    type: string
    label: HTMLElement
    walk(): void
}

type Cat = {
    type: number
    label: HTMLInputElement
    walk(): void
}

function testControlFlowInner(a: Dog | Cat) {
    a.walk()
    if (typeof a.type === 'string') {
        // 因此在这里的话,ts在已知type为string的情况下
        // 也只能推导出a.label的类型为HTMLElement|HTMLInputElement
        console.log(a.label)
    }
}

function testControlFlow() {
    // 因为并集,可以是string的type,加上HTMLInputElement的label
    testControlFlowInner({
        type: '1',
        label: new HTMLInputElement(),
        walk: () => {
            console.log('walk')
        },
    })
}

ts支持控制流的类型推导,它就像人一样推导当前的类型是什么


// HTMLInputElement是HTMLElement的子类
// 但是如果我们将type用字面值来表达的话
type Dog2 = {
    type: 'a'
    label: HTMLElement
    walk(): void
}

type Cat2 = {
    type: 'b'
    label: HTMLInputElement
    walk(): void
}

function testControlFlowInner2(a: Dog2 | Cat2) {
    a.walk()
    if (a.type === 'b') {
        // ts能推导出来label是HTMLInputElement
        console.log(a.label)
    }
    if (a.type === 'a') {
        // ts能推导出来label是HTMLElement类型
        console.log(a.label)
    }
}

// FIXME,我认为这里依然不是很严谨,具体看书P157
function testControlFlow2() {
    // 因为并集,可以是string的type,加上HTMLInputElement的label
    testControlFlowInner2({
        type: 'a',
        label: new HTMLInputElement(),
        walk: () => {
            console.log('walk')
        },
    })
}

迷惑的地方出现了,当一个类型的成员是字面值的时候,经过type操作判断,可以推导出了另外一个成员的类型。但是,对于成员是非字面值的时候,就无法推导另外一个成员的类型。

4 数组与元组类型

代码在这里

数组也是常用的一个类型,注意,数组也是属于object类型的

4.1 初始数组推导

function testArray() {
    // 推导为number[]类型
    const a = [1, 2, 3]

    a.push(3)
    // 下面这一句会自动报错,不能将string放入number[]类型
    // a.push('4')
    console.log(a)

    // 可以显式加入类型
    const b: string[] = ['1', '2', '3']

    // 推导为 (number | string)[]类型
    const c = [1, 'c']

    console.log(b)
    console.log(c)
}

对于数组有初始行为,或者有显式的数组类型时,ts会给出准确的数组类型

4.2 无初始数组推导

function testAutoDetectArray() {
    // 直接声明时,类型为any[]
    const a = []

    a.push(1)
    a.push('2')
    console.log(a)

    // 返回值推导为 (number | string)[]类型
    return a
}

但是,对于数组没有初始数据时,ts会推导为any[]类型。然后,会在最终流程将所有数据组合起来分析,这个数组应该是什么类型。(这点也牛逼)

4.3 元组类型


function testTuple() {
    // 元组,可以修改,但是将
    let a: [number, string, string]
    a = [1, '2', '3']
    a[0] = 4

    // 不能将类型不符的元组赋值过去,这样会报错
    // a = ['1', '2', '3']

    // 不能将其他长度的元组赋值过去,这样会报错
    // a = [1, '2', '3', '5']

    // 不能读取超越长度的index
    // console.log(a[3])

    // 可以推入新的元素进去,然后删除元素
    // 这里的设计并不好
    a.push(4)
    a.splice(0, 2)
}

元组类型其实就是固定长度的数组,而且它每个位置固定数据类型。但是,ts竟然支持元组的push和splice操作,这点也是较为迷惑

5 枚举

代码在这里

枚举类型,可能是typescript的唯一败笔,我们来看看为什么这么说。

5.1 整数枚举

enum NormalEnum {
    Red,
    Green,
}
function testEnum() {
    const a = NormalEnum.Red // 这个值为0

    const b = NormalEnum[0] // 这个值为Red
    const c = NormalEnum[1] // 这个值为Green
    const d = NormalEnum[2] // 这个值为undefined,TS并没有在编译时报错,这个enum的设计并不好
    console.log(a, b, c, d)
}

默认的枚举是整数值,所以我们能用整形来索引得到枚举,但是即使传入的数字不是有效值,ts也不会报错

5.2 常量整数枚举

// 枚举值默认是从0开始
const enum NormalEnum2 {
    Red,
    Green,
}

function testEnum2() {
    const a = NormalEnum2.Red // 这个值为0

    // 使用了const enum以后,只能用string来访问enum。不能用整数来访问enum
    // 以下都会报错
    // const b = NormalEnum2[0]
    // const c = NormalEnum2[1]
    // const d = NormalEnum2[2]
    console.log(a)
}

// 枚举值默认是从0开始
const enum NormalEnum3 {
    Red,
    Green,
}

function testEnum3Inner(a: NormalEnum3) {
    console.log(a)
}

function testEnum3() {
    // 把其他枚举体传过去是不行的,会报错,即使值一样,枚举不是结构类型
    // testEnum3Inner(NormalEnum.Red)

    // 安全可靠,值为NormalEnum
    testEnum3Inner(NormalEnum3.Red)

    // 这里竟然能将整数当枚举传递过去,一点都安全,根本就没有整数为200的枚举
    testEnum3Inner(200)
}

使用了常量整数枚举以后,避免了整数索引枚举的问题,只能用字符串来索引枚举。但是,当枚举类型作为函数参数的时候,竟然又可以用不合法的整数值来传递了。

这个设计,真的是,一言难尽。

5.3 常量字符串枚举

// 这个写法最好,但是对开发者要求太高
const enum NormalEnum4 {
    Red = 'red',
    Green = 'green',
}

function testEnum4Inner(a: NormalEnum4) {
    console.log(a)
}

function testEnum4() {
    // 安全可靠,值为NormalEnum
    testEnum4Inner(NormalEnum4.Red)

    // 要将枚举的值手动全部改为string,才是安全的
    // testEnum4Inner(200)

    // 手动传入字符串也不行
    // testEnum4Inner('red')
}

只有使用常量字符串枚举的时候,这个才是可靠的。但是,ts的枚举竟然支持字符串枚举,与整数枚举的混合写法。

const enum NormalEnum4 {
    Red = 'red',
    Green = 123,
}

如果这样写代码的话,那么之前出现的问题还会重新出现一次。没有编译时的报错,也没有ESLint的报错,这种属于悄悄地发生错误,一点都不可靠。

5.4 字面值常量

type NormalEnum5 = 'red' | 'green'

function testEnum5Inner(a: NormalEnum5) {
    console.log(a)
}

function testStringAsEnum() {
    // 传其他字符串也行
    testEnum5Inner(NormalEnum4.Red)

    // 支持手动传入字符串
    testEnum5Inner('red')

    // 传入整数当然是不行的
    // testEnum5Inner(200)
}

使用字面值常量代替枚举才是正确的用法,完全不用担心编译时传错了数据的问题。而且,字面值常量还支持in ,keyof等Mapped Type特性,还有自动的全面性检查的特性,非常好用,枚举就没有这点好处了,后面会介绍到这点。

强烈要求禁止使用枚举,而改用字面值常量来代替枚举。

6 函数类型

代码在这里

函数类型,在js和ts中它是属于Object类型。并且,在js中,函数甚至还能有自己的成员变量与方法。这一点ts也保留下来了。

6.1 基础

6.1.1 普通函数

// 普通命名函数
function go1(name: string) {
    console.log(`go:${name}`)
}

// 匿名函数赋值给变量
const go2 = (name: string, age: number) => {
    console.log(`name:${name},age:${age}`)
}

//创建一个function type
type goType = (a:string,b:number)=>void

function testFunction() {
    go1('a')
    go2('fish', 200)
    let a:goType = go2
}

命名与匿名函数,简单,注意fuction type类型的声明

6.1.2 默认参数与可选参数

// 默认参数与可选参数
function go3(message = 'Hello', size?: number) {
    console.log(`message,${message},size:${size}`)
}

function testFunction2() {
    go3()
    go3('uu')
    go3('uu', 10)
}

默认参数与可选参数

6.1.3 不定参数

// 不定参数的写法
function go4(format: string, ...args: any[]) {
    console.log(format, ...args)
}

function testFunction3() {
    go4('a')
    go4('b', 1, '2')
    go4('c', 1, 'bc', 'g')
}

不定参数

6.1.4 this参数指定

type MyGo = {
    walk(): string
}

// 注解this参数
// 函数的返回值标注
function go5(this: MyGo, message: string): string {
    return `{${this.walk()},${message}}`
}

function testFunction4() {
    // 编译错误,因为this参数为null
    // 直接调用函数时,this参数取的是外部环境的this
    // go5('ab')

    const myObj = {
        walk: (): string => 'mm',
    }
    // 用call方法调用函数,可以指定this
    const result = go5.call(myObj, 'tt')
    console.log(result)
}

函数其实都是有一个this参数的,一般情况传入的this的实际参数就是null,形式参数我们默认是没有标注的。但是在ts中,我们可以显式地标注这个参数,并且要求ts调用这个函数时,this参数都要满足我们指定的要求。

6.2 生成器与迭代器

生成器是一个可以生成数值的函数,它可以生成有限的数据,也可以生成无限的数据,这个在ts中是没有限制的。迭代器是一个对象,它含有一个值为生成器的属性。我们要注意两者的区别

6.2.1 生成器

// 生成器函数,签名上要有*方法,每次返回值用yield关键字
function* go6() {
    let n = 10
    while (n >= 0) {
        yield n
        n -= 1
    }
}

function testFunction5() {
    const generator = go6()
    // 每次拿值就用它的next方法,只要done为false就永远有值
    // 生成器函数的问题在于,它不支持for in的方法
    let singleResult = generator.next()
    while (!singleResult.done) {
        console.log(`generator :${singleResult.value}`)
        singleResult = generator.next()
    }
}

用function*指定的就是生成器函数,生成器函数都会自动带有一个next方法,来不断获取下一个数值

6.2.2 迭代器

// 这个是迭代器,不是生成器
function createGo7() {
    // 迭代器是一个对象,它还有一个key为Symbol.iterator的属性,该属性的值为生成器函数
    return {
        *[Symbol.iterator]() {
            let n = 10
            let i = 0
            while (n >= 0) {
                yield [i, `value${n}`]
                n -= 1
                i += 1
            }
        },
    }
}

function testFunction6() {
    const iterator = createGo7()
    // 满足要求的迭代器,就可以使用for of语法来遍历
    for (const [i, value] of iterator) {
        console.log(i, value)
    }
}

迭代器是一个对象,它含有一个key为Symbol.iterator的属性,这个属性的值为生成器。满足这样约束的对象就是迭代器,于是就可以用普通的for in和for of来迭代这个对象了,这其实是一个语法糖

6.3 函数也是一个对象

function testObjectFunction1() {
    // 我们可以用object的形式来声明一个函数类型
    // 不要用in作为参数名,因为in是关键字
    type Dog = {
        (mk: number): string
    }

    // 直接将函数赋值给这个Dog类型是没问题的
    const a: Dog = function (mk: number): string {
        return `${mk}bb`
    }
    // 输出的是1bb
    console.log(a(1))

    // 输出的函数名字,就是a本身
    console.log(a.name)
}

我们可以先声明一个object,它还有一个匿名的函数。那么我们可以用函数来赋值到这个object类型,是没问题的。

function testObjectFunction2() {
    // 既然object可以表达function,那么它也可以带自己的字段和成员
    type Dog = {
        (): string
        next(): string
        size: number
    }

    function mm(): string {
        return 'Hello World'
    }
    mm.next = () => '123'
    mm.size = 100

    const a: Dog = mm
    a()
    console.log(a.next())
    console.log(a.size)
}

甚至我们可以声明一个称为Dog的类型,它有方法和成员,也有一个匿名的函数。那么这个Dog的类型,完全是可以用一个命名函数去赋值它的,所以能看到函数也是可以有自己的成员变量和方法的

6.4 函数重载

// 一个函数的声明,代表该函数的重载
function GoFishing(input: number): string
function GoFishing(input: string): string
function GoFishing(): string

// 对这个函数的实现,只能有一个实现
function GoFishing(input?: number | string): string {
    if (typeof input === 'number') {
        return 'mm'
    }
    return 'gg'
}

function testOverloadFunction1() {
    // 定义一个固定名字的函数
    const result1 = GoFishing(1)
    const result2 = GoFishing('2')
    const result3 = GoFishing()

    console.log(result1, result2, result3)
}

函数重载是多个函数声明,一个函数实现。注意的是,同一个函数的多个函数重载的时候,只能有一个杉树实现,该实现必须满足这个函数的多个声明的约束。

function testOverloadFunction2() {
    // 定义函数的类型,有多个声明,表明是可重载
    type MyOverloadFunc = {
        (input: number): string
        (input: string): string
    }

    // 实现这个函数的重载实现,注意,与其他类型不同的,函数可以有多个声明,但只能有一个实现
    const callFunc: MyOverloadFunc = function (input: number | string): string {
        if (typeof input === 'number') {
            return `mm${input}`
        }
        return `cc${input}`
    }

    const result1 = callFunc(1)
    console.log(result1)
    const result2 = callFunc('fish')
    console.log(result2)
}

另外一种声明函数的方法,是以object的方式来声明函数重载,只是换个写法而已。

6.5 泛型函数(多态)

ts也支持泛型函数,而且结合类型的推导,与函数重载,他的功能会更加强大

6.5.1 不同的字面值输入,输出不同的类型

class Dog {}
class Cat {}
class Fish {}
function GoPlay(input: 'a'): Dog
function GoPlay(input: 'b'): Cat
function GoPlay(input: 'c'): Fish

function GoPlay(input: string): Dog | Cat | Fish {
    if (input === 'a') {
        return new Dog()
    }
    if (input === 'b') {
        return new Cat()
    }
    if (input === 'c') {
        // 这个实现并不可靠,因为实现
        // 只能返回new Dog()依然能编译成功
        return new Fish()
    }
    throw new Error('mm')
}

function testGenericFunction1() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoPlay('a')
    const result2 = GoPlay('b')
    const result3 = GoPlay('c')

    console.log('GoPlay', result1, result2, result3)
}

例如GoPlay函数,就是输入为不同的字面值类型,输出为不同的类型。我们希望在编译时就能计算这种不同的结果,以避免运行时的错误(例如,输入参数b,但是得到返回值以后,调用它的Dog的方法)。这点,我们可以通过函数重载来实现

// 使用keyof的实现也并不可靠
type Dance = {
    a: Dog
    b: Cat
    c: Fish
}
function GoDance<T extends keyof Dance>(input: T): Dance[T] {
    if (input === 'a') {
        return new Dog()
    }
    if (input === 'b') {
        return new Cat()
    }
    if (input === 'c') {
        // 这个实现并不可靠
        // 只能返回new Dog()依然能编译成功
        return new Fish()
    }
    throw new Error('mm')
}

function testGenericFunction2() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoDance('a')
    const result2 = GoDance('b')
    const result3 = GoDance('c')

    console.log('GoDance', result1, result2, result3)
}

另外一种方法是用keyof和泛型来实现,这是一个相当重要的技巧。在书本的P208中有大量的使用这种技巧来构建安全的异步和回调方法

6.5.2 不同的类型输入,输出不同的类型

class Dog {}
class Cat {}
class Fish {}
function GoPlay(input: number): Dog
function GoPlay(input: string): Cat
function GoPlay(): Fish

function GoPlay(input?: number | string): Dog | Cat | Fish {
    if (typeof input === 'number') {
        return new Dog()
    }
    if (typeof input === 'string') {
        // 这个实现并不可靠
        // 只能返回new Dog()依然能编译成功
        return new Cat()
    }
    return new Fish()
}

function testGenericFunction1() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoPlay(1)
    const result2 = GoPlay('b')
    const result3 = GoPlay()

    console.log('GoPlay', result1, result2, result3)
}

我们可以用函数重载来实现,不同的类型输入,有不同的类型输出。而且,这些类型输出在编译时就能推导出来了。

// 使用keyof无法实现以上的功能,要用泛型判断
type DanceResultType2<T> = T extends number ? Dog : Cat
type DanceResultType<T> = T extends undefined | null | never | void
    ? Fish
    : DanceResultType2<T>
function GoDance<T extends number | string>(input?: T): DanceResultType<T> {
    if (typeof input === 'number') {
        return new Dog()
    }
    if (typeof input === 'string') {
        // 这个实现并不可靠
        // 只能返回new Dog()依然能编译成功
        return new Cat()
    }
    return new Fish()
}

function testGenericFunction2() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoDance(1)
    const result2 = GoDance('b')

    // 这里推导不好,只能推导出Dog|Cat类型,而不是Fish类型
    const result3 = GoDance()

    console.log('GoDance', result1, result2, result3)
}

如果用泛型实现的话,就不能用keyof操作符了,因为输入的参数类型不是字面值类型,是不同的类型。这个时候,要用类型判断表达式。但是以上的实现中,有点遗憾的是,对于无参数的输入,推导出来的返回值类型是不对的。

// 使用泛型加重载就能解决这个问题了
type FuckResultType<T> = T extends number ? Dog : Cat
function GoFuck<T extends number | string>(input: T): FuckResultType<T>
function GoFuck<T>(): Fish

function GoFuck<T>(input?: T): Fish | FuckResultType<T> {
    if (typeof input === 'number') {
        return new Dog()
    }
    if (typeof input === 'string') {
        return new Cat()
    }
    return new Fish()
}

function testGenericFunction3() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoFuck(1)
    const result2 = GoFuck('b')

    // 现在能推导出来result3是Fish类型了
    const result3 = GoFuck()

    console.log('GoFuck', result1, result2, result3)
}

我们可以用泛型+重载的方法来解决这个问题,这个时候无参数的输入,推导出来的返回值类型是正确的

6.5.3 不同的类型输入,输出不同的字面值

function GoPlay(input: number): 'mm'
function GoPlay(input: string): 'gg'
function GoPlay(input: object): 'kk'
function GoPlay(): 'll'

function GoPlay(input?: number | string | object): 'mm' | 'gg' | 'kk' | 'll' {
    if (typeof input === 'number') {
        return 'mm'
    }
    if (typeof input === 'string') {
        return 'gg'
    }
    if (input instanceof Object) {
        return 'kk'
    }
    return 'll'
}

function testGenericFunction1() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoPlay(1)
    const result2 = GoPlay('b')
    const result3 = GoPlay()
    const result4 = GoPlay({})

    console.log('GoPlay', result1, result2, result3, result4)
}

同样地,我们可以用函数重载来实现,不同的类型输入,能推导出不同的字面值类型

// 使用泛型加重载就能解决这个问题了
type FuckResultType<T> = T extends number
    ? 'mm'
    : T extends string
    ? 'gg'
    : 'kk'

function GoFuck<T extends number | string | object>(input: T): FuckResultType<T>
function GoFuck<T>(): 'll'

function GoFuck<T>(input?: T): 'll' | FuckResultType<T> {
    if (typeof input === 'number') {
        return 'mm' as FuckResultType<T>
    }
    if (typeof input === 'string') {
        return 'gg' as FuckResultType<T>
    }
    if (input instanceof Object) {
        return 'kk' as FuckResultType<T>
    }
    return 'll'
}

function testGenericFunction2() {
    // 定义一个固定名字的函数,在编译时就能推导出它的结果
    const result1 = GoFuck(1)
    const result2 = GoFuck('b')
    const result3 = GoFuck()
    const result4 = GoFuck({})

    console.log('GoFuck', result1, result2, result3, result4)
}

同样地,用函数泛型+函数重载也能解决这个问题

7 类

代码在这里

原来的js系统中是没有类这个说法,类仅仅是通过对象和函数模拟的。要注意的是,在ts与js中,类不仅是一个类型,它本身还是一个值(你可以将类本身赋值给一个变量)。这跟Java和Golang都不同。

7.1 基础

7.1.1 构造器,继承,方法与成员

class Game {
    // 构造器含有private,protected,public的表明它不仅是构造器参数,还会自动被赋值为成员
    constructor(private isStart: boolean, private playerSize: number) {}

    // 方法的定义
    public getIsStart() {
        return this.isStart
    }

    public getPlayerSize() {
        return this.playerSize
    }
}

// 继承用,extends
class PieceGame extends Game {
    constructor(
        public pieceSize: number,
        isStart: boolean,
        playerSize: number
    ) {
        // 调用父类的构造器
        super(isStart, playerSize)
    }
}

class AnimalGame extends Game {
    // 构造器的animalSize是没有访问标志的,没有public,没有private,也没有protected,所以这个仅仅是构造器参数,不是成员
    constructor(animalSize: number, isStart: boolean, playerSize: number) {
        super(isStart, playerSize)
    }
}

function testClass1() {
    const game = new Game(true, 10)

    // isStart是private访问,不能获取
    // console.log(game.isStart)
    console.log(game.getIsStart()) // 这个值为true
    console.log(game.getPlayerSize()) // 这个值为10

    const pieceGame = new PieceGame(10, false, 70)
    // pieceSize是public权限,所以可以访问
    console.log(pieceGame.pieceSize)
    console.log(pieceGame.getIsStart())
    console.log(pieceGame.getPlayerSize())

    const animalGame = new AnimalGame(10, true, 80)
    // animalSize是不是成员,它仅仅是个构造器参数而已
    // console.log(animalGame.animalSize)
    console.log(animalGame.getIsStart())
    console.log(animalGame.getPlayerSize())
}

注意ts中的特殊写法,如果构造器上有访问标志,那么这个参数就是成员变量的一部分

7.1.2 抽象类


// 抽象类
abstract class Animal {
    // 抽象类也可以有自己的方法
    public go() {
        if (this.canGo() === false) {
            console.log('this animal can not walk!')
            return
        }
        console.log('animal walk')
    }

    abstract canGo(): boolean
}

class Dog extends Animal {
    public canGo() {
        return true
    }
}

class Fish extends Animal {
    public canGo() {
        return false
    }
}

function testClass2() {
    // 抽象类不能实例化
    // const animal = new Animal()

    // 非抽象类才可以实例化
    const dog = new Dog()
    dog.go()

    const fish = new Fish()
    fish.go()
}

抽象类不能实例化,但是它可以带具体的方法

7.2 接口

7.2.1 接口实现

interface Walker {
    walk(): void
}

// 接口实现用implements而不是extends
class Dog implements Walker {
    public walk() {
        console.log('dog walk')
    }
}

class Cat implements Walker {
    public walk() {
        console.log('cat walk')
    }
}

function testClassInterface1() {
    const a = new Dog()
    a.walk()

    const b = new Cat()
    b.walk()
}

接口的基础使用,指定一个类实现了某个接口,用implements

7.2.2 接口与类型

type MK = {
    swim(): void
}

// 在ts中,你甚至可以用type来作为类的implements指定,像interface一样
class Fish implements MK {
    swim(): void {
        console.log('fish swim')
    }
}

interface Swimer {
    swim(): void
}

function testClassInterface2() {
    const a = new Fish()
    a.swim()

    // 即使Fish类型没有显式实现Swimer接口,也可以赋值给Swimer接口,结构性类型系统
    // implements其实是一种类型的声明性约束,在语法层要求代码实现了这个接口而已
    const b: Swimer = a
    b.swim()
}

接口其实只是类型的一种语法糖,对一个类标志implements 某个接口,仅仅为告诉编译器,帮我检查一个这个类,确保它满足了这个接口而已。这个类的实例依然可以赋值到其他没有显式标志的接口。

7.2.3 接口派生

// SwimerAndWalker接口同是extends了两个
interface SwimerAndWalker extends Swimer, Walker {}

// type类型的话,可以用&运算来模拟extends的接口
type MM = MK & Walker

class Frog implements Swimer, Walker {
    public swim(): void {
        console.log('frog swim')
    }

    public walk(): void {
        console.log('frog walk')
    }
}

function testClassInterface3() {
    // 赋值失败,因为Fish只会Swim,不会Walk
    // const a: SwimerAndWalker = new Fish()

    const a: SwimerAndWalker = new Frog()
    const b: MM = new Frog()
    a.swim()
    a.walk()

    b.swim()
    b.walk()
}

我们可以指定组合多个接口为一个新接口,它就像可以对多个类型执行and操作生成一个新类型一样。

7.3 类也是一个值

ts的类,不仅是一个类型,而且还是一个值,这被称为伴生对象模式P170.

class Dog {
    // 属性默认为public
    name: string
    constructor(label: string) {
        this.name = label
    }

    static showName: string = 'I am Dog Name'
    static getGlobalSize(): number {
        return 10
    }
}

function testClassFunction1() {
    const dog = new Dog('mm')
    console.log(dog.name)
    // Dog的静态变量,与静态方法
    console.log(Dog.showName)
    console.log(Dog.getGlobalSize())

    // 静态方法不能通过实例来拿取
    // console.log(dog.showName)
}

我们可以指定类的静态方法与成员

// 在ts的实现来看,Class其实是一个Object,一个含有new方法的Object而已
type MyType = {
    new (...args: any[]): object
    showName: string
    getGlobalSize(): number
}
function testClassFunction2() {
    // 看,Dog类本身就是一个实例,它可以赋值给一个Type
    const a: MyType = Dog

    console.log(a.showName)
    console.log(a.getGlobalSize())

    // 要使用MyType的new方法,就需要先把它extends了,然后创建这个对象出来
    class MM extends a {
        constructor(...args: any[]) {
            super(...args)
        }
    }

    // 创建了这个对象
    const mm = new MM('gg')

    // 可以拿到这个MM底层对应的name
    console.log((mm as Dog).name)
}

而类本身也可以赋值给另外一个类型,甚至作为值本身的类,可以动态地为他扩展其他方法与成员

7.4 装饰器

class Builder {
    private data: object | null = null
    private method: 'get' | 'post' | null = null
    private url: string | null = null
    public name: string

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

    public setMethod(method: 'get' | 'post'): this {
        this.method = method
        return this
    }

    public setData(data: object): this {
        this.data = data
        return this
    }

    public setURL(url: string): this {
        this.url = url
        return this
    }

    public send() {
        console.log('send', this.method, this.url, this.data)
    }
}

type ClassConstructor<T> = new (...args: any[]) => T

function decorator<T extends ClassConstructor<{}>>(Constructor: T) {
    return class extends Constructor {
        constructor(...args: any[]) {
            super(...args)
        }

        go() {
            console.log('mm')
        }
    }
}

export default function testDecorator() {
    const builder = new Builder('mm')
    builder.setData({ a: 3 }).setMethod('get').setURL('www.baidu.com').send()

    const constructor = decorator(Builder)
    // 注意newData的类型为decorator的匿名class&Builder
    const newData = new constructor('mj')
    newData.go()
}

当理解到了类其实是一个值的时候,装饰器模式就容易理解了

7.5 this参数指定

// 需要两个类型,T类型为object,K类型必须来自于T类型的key
type Picker<T, K extends keyof T> = {
    // 对于K类型的所有值,建立一个字段映射类型
    [key in K]: T[key]
}

// Picker<BuildableRequest,'method'>,相当于声明{'method':'get'|'post'}类型
// TS中的类型是结构类型,不是声明类型,只要类型中实现了这个接口结构,那么类型就可以实现了这个接口。
interface BuildableRequest {
    hasData: boolean
    hasMethod: boolean
    hasUrl: boolean
}

// 例如一个Picker<BuildableRequest,'hasMethod'>&Picker<BuildableRequest,'hasData'>&Picker<BuildableRequest,'hasUrl'>,就代表它实现了这个BuildableRequest接口

class Builder {
    data?: object
    method?: 'get' | 'post'
    url?: string

    public setMethod(
        method: 'get' | 'post'
    ): this & Picker<BuildableRequest, 'hasMethod'> {
        return Object.assign(this, { method: method, hasMethod: true })
    }

    public setData(data: object): this & Picker<BuildableRequest, 'hasData'> {
        return Object.assign(this, { data: data, hasData: true })
    }

    public setURL(url: string): this & Picker<BuildableRequest, 'hasUrl'> {
        return Object.assign(this, { url: url, hasUrl: true })
    }

    // 重写this参数,this需要为Builder,同时也要满足BuildableRequest
    public send(this: BuildableRequest & Builder) {
        console.log('send', this.method, this.url, this.data)
        return this
    }
}

export default function testChangeClassThis() {
    const builder = new Builder()
    builder.setData({ a: 3 }).setMethod('get').setURL('baidu.com').send()

    builder.setData({ a: 3 }).setMethod('get').setURL('baidu.com').send()

    // 缺少任意一个set,ts都会报错
    /*
    builder.setData({a:3})
        .setMethod('get')
        .setURL("baidu.com")
        .send();
    */
}

方法中的this参数我们是默认不写的,当然我们也可以显式指定this参数的约束,从而让ts编译器保证调用这个方法需要满足什么条件。

7.6 this与方法

class User {
    private name: string
    constructor(name: string) {
        this.name = name
    }

    // 直接声明的方法
    walk() {
        console.log((this ? this.name : '') + 'walk')
    }

    // 使用箭头声明的方法,默认就会绑定当前的this
    jump = ():number => {
        console.log(this.name + 'jump')
        return 123;
    }
}

let gg:Person | undefined;

abstract class Person{
    constructor(){
        console.log('constructor begin --- ');
        //能调用得到子级Man.walk.
        this.walk();
        //失败,这个时候调用的是父级的jump
        this.jump();
        console.log('constructor end --- ');
        gg = this;
    }
    
    //直接声明的方法,没有箭头的函数,
    walk(){
        console.log('person walk');
    }

    //有箭头的函数
    //加入private修饰,我们能避免jump方法被override,TS没有提供final的修饰符 
    jump = ()=>{
        console.log('person jump');
    }
}

/*
箭头函数的实现,运行时定义函数。没有箭头函数的实现,一开始就定义在protype链的函数
class Person {
    constructor() {
        this.jump = () => {
            console.log('person jump');
        };
        console.log('constructor begin --- ');
        ...
    }
    walk() {
        console.log('person walk');
    }
}
*/

class Man extends Person{
    constructor(){
        super();
    }

    // 直接声明的方法
    override walk() {
        super.walk();
        console.log('man walk');
    }

    //有箭头的函数,override只检查声明,没有检查箭头的问题
    jump = ()=>{
        //调用super会失败,箭头函数里面会丢失super
        //super.jump();
        console.log('man jump');
    }
}

/*
Man.jump箭头函数的实现,在super运行完成以后,才去定义this.jump.
class Man extends Person {
    constructor() {
        super();
        this.jump = () => {
            //调用super会失败,箭头函数里面会丢失super
            //super.jump();
            console.log('man jump');
        };
    }
    walk() {
        super.walk();
        console.log('man walk');
    }
}
*/

function testClassMethodThis() {
    const a = new User('fish')

    // 这样的话附带有this
    a.walk()

    // 这样做是不安全的,会丢失this指针
    // 但是,TS并没有报错,相当诡异
    const b: () => void = a.walk
    b()

    // 这样的话也附带有this
    a.jump()

    // 即使只取方法,也有this,因为是箭头函数
    const c: () => void = a.jump
    c()

    const man = new Man();
    console.log('constructor outer begin ---');
    man.walk();
    man.jump();
    console.log('constructor outer end ---');
    gg?.jump();
    console.log('gg end ---');
   
}

export default testClassMethodThis

要点如下:

  • 箭头方法默认是绑定了this,而非箭头方法则受限于调用时的方式,有可能会丢失this
  • 箭头方法的缺点在于,不能调用super,在基类构造的时候,无法调用子类的同名方法。

换句话说,一旦使用箭头方法以后,就失去了继承方法的灵活性。所以,我们推荐,箭头只提供在对外被绑定的事件上,并且最好加上private修饰,从而避免被子类override.

7.7 泛型

7.7.1 泛型类

class Stack<T> {
    private data: T[]

    constructor() {
        this.data = []
    }

    public push(single: T): void {
        this.data.push(single)
    }

    public pop(): T {
        if (this.data.length === 0) {
            throw new Error('stack is empty')
        }
        const result = this.data[this.data.length - 1]
        this.data.splice(this.data.length - 1, 1)
        return result
    }
}

function testGeneric1() {
    const numberStack = new Stack<number>()
    numberStack.push(1)
    numberStack.push(2)

    console.log(numberStack.pop())
    console.log(numberStack.pop())
    try {
        console.log(numberStack.pop())
    } catch (e) {
        console.log('pop fail')
    }
}

泛型类的例子,还是相当简单的

7.7.2 泛型类下的泛型方法

class ObjectWrapper<T extends object> {
    private data: T

    constructor(data: T) {
        this.data = data
    }

    public get<G extends keyof T>(single: G): T[G] {
        return this.data[single]
    }
}

function testGeneric2() {
    const a = new ObjectWrapper({
        name: 'fish',
        size: 2,
    })
    // 自动推导result1的类型为string,result2的类型为number
    const result1 = a.get('name')
    const result2 = a.get('size')

    console.log(result1, result2)
}

泛型类下的泛型方法,方法也是可以带上泛型的,这样做更加安全

8 高级特性

代码在这里

8.1 全面性检查

type Animal = 'dog' | 'cat' | 'fish'

function getAnimalOrdal(input: Animal): number {
    // 这样写不好,会漏掉fish的常量
    let number = 0
    if (input === 'dog') {
        number = 1
    } else if (input === 'cat') {
        number = 2
    }
    return number
}

function getAnimalOrdal2(input: Animal): number {
    // 这样写是最好的,漏掉一个情况,都会报错
    switch (input) {
        case 'dog':
            return 1
        case 'cat':
            return 2
        case 'fish':
            return 3
    }
}

function testFullCheck() {
    getAnimalOrdal('dog')
    getAnimalOrdal2('cat')
}

switch语句有自动的全面性检查分析,保证我们不会漏掉其中一个分支,相当可靠

8.2 MappedType

MappedType的意思是,将一个类型为依据,转换到另外一个类型,这是类型运算的另外一种操作符,有时候相当有用省事

8.2.1 in操作符

type Day = 'Mon' | 'Tues' | 'Wes'
type Result = 12 | 34

// mappedType是ts弥补静态类型的方法,它的意思是创建一个类型,该类型含有Day的所有key,Value类型必须为Result类型
// in 操作符的意思是,将Day这种字面值类型并集的每一个元素,拿出来作为object类型的key类型
type MyMappedType = {
    [K in Day]: Result
}

function testMappedType1() {
    const a: MyMappedType = {
        Mon: 34,
        Tues: 12,
        // 如果缺少一个成员,都会报错
        Wes: 12,
    }
    console.log(a)
}

使用in操作符,我们可以将字面值常量并集,转换为object类型的key

// 建立一个MyRecord类型,它其实就是原生Record类型的实现
// 就是刚才的MyMappedType类型的泛型版本而已
type MyRecord<T extends keyof any, U> = {
    [K in T]: U
}

function testMappedType2() {
    const a: MyRecord<Day, Result> = {
        Mon: 34,
        Tues: 12,
        // 如果缺少一个成员,都会报错
        Wes: 12,
    }
}

in操作符也支持泛型下的使用

8.2.2 keyof操作符

type Account = {
    id: number
    isEmployee: boolean
    notes: string[]
}

// in 操作符是将字面值类型的并集转换为object的key类型
// 那么keyof 操作符就是反过来,将object的key类型转换为字面值类型的并集
type AccountKey = keyof Account

function testMappedType3() {
    let a: AccountKey
    a = 'id'
    a = 'isEmployee'
    a = 'notes'
    // 这里会报错,因为'id3'不是Account的key类型,不在AccountKey字面值中
    // a = 'id3'
    console.log(a)
}

keyof操作符就是反过来,它将object的key类型,转换为字面值常量的并集

8.2.3 in与keyof操作符组合

// 结合in操作符,与keyof类型,我们可以做到将object类型,转换为另外一种object类型
// 现在AccountOption类型的每个字段都是可选的
type AccountOption = {
    [K in keyof Account]?: Account[K]
}

function testMappedType4() {
    // Account类型的每个字段都是必选的
    const a: Account = {
        id: 1,
        isEmployee: true,
        notes: ['fish'],
    }
    console.log(a)

    // AccountOption类型的每个字段都是可选的
    const b: AccountOption = {
        id: 1,
        isEmployee: true,
    }
    console.log(b)
}
// 同理,还有预定义好的方法,例如Record,Partial,Required,ReadOnly和Pick等等

组合in与keyof操作符,我们可以以对象类型为依据,转换为另外一个对象类型

8.2.4 typeof 操作符

const MyAccount = {
    a:()=>{
        return "123"
    },
    b:()=>{
        return 123
    }
}

//typeof 操作符,不仅可以在运行时使用,编译时也能用来使用
type MyAccountType = typeof MyAccount;

type MyAccountKeyType = keyof MyAccountType;

function testMappedType5(){
    function ok(a:MyAccountKeyType){
        console.log(a);
    }
    ok('a');
    ok('b');
}

typeof操作符不仅可以在运行时使用,编译时也可以使用

8.3 条件类型

extends是一种类型的编译时条件选择操作

8.3.1 三元操作符

class NumberWrapper {}
class StringWrapper {}

// extends方式是一种编译时的条件类型判断
type WrapperResultType<T> = T extends number ? NumberWrapper : StringWrapper
function wrapper<T extends number | string>(input: T): WrapperResultType<T> {
    if (typeof input === 'number') {
        return new NumberWrapper()
    } else {
        return new StringWrapper()
    }
}

function testTypeCheck1() {
    // 编译时就可以进行类型的判断操作,这其实是函数重载的另外一种实现,但比函数重载强大多了
    const result1 = wrapper(1)
    const result2 = wrapper('hello')
    console.log(result1, result2)
}

只支持三元操作符

8.3.2 元素类型推导

// infer U是一种特殊的类型判断,它可以提取数组中的元素类型
type ElemType<T> = T extends (infer U)[] ? U : T

function testTypeCheck2() {
    // 可以看到R1,R2,R3的实际类型是啥
    type R1 = ElemType<number>
    type R2 = ElemType<number[]>
    type R3 = ElemType<string[]>
}

配合infer操作符,可以在编译时计算数组的元素类型

8.3.3 分配率

// extends操作符是用分配率来执行的,以保持输入时的并集
// WithOut<number | string,string> = WithOut<number,string> | WithOut<string ,string>
type WithOut<T, U> = T extends U ? never : T

function testTypeCheck3() {
    //推导为number
    type R1 = WithOut<number | string, string>
    //推导为number | string
    type R2 = WithOut<number | object | string, string>
}
function testTypeCheck() {
    testTypeCheck1()
    testTypeCheck2()
}

extends的执行,就像是乘法分配率一样

8.4 类型强制转换

function testTypeConvert() {
    // 强行将number类型看成是string类型,这种看成是编译时的,仅仅是为了编译公国而已
    // 运行时没有执行任何的类型转换
    const a = 123 as unknown as string
    try {
        console.log(a.toUpperCase())
    } catch (e) {
        console.log('出错,类型本来就不是string')
    }
}

强制类型转换,as操作符,这是一种不安全的做法,尽量避免使用

function testNotNull() {
    let a: string | undefined

    // ?号简要判断,这是运行时判断,只有a不是undefined的时候,才执行后面的toUpperCase
    a?.toUpperCase()

    // !号的是强行判断,仅仅是编译通过而已,没有任何的运行时判断
    try {
        a!.toUpperCase()
    } catch (e) {
        console.log('出错,类型本来就是undefined')
    }
}

强制类型非空,?是可选操作,运行时会检查,可恰当使用。!是强行指定,不安全的做法,尽量避免使用。

8.5 类型扩展

declare global {
    interface String {
        convertSheep(): string
    }
}

String.prototype.convertSheep = function (this: string) {
    return '【' + this + '】'
}

export default {}

我们可以为原生类型在prototype上扩展,增加一个新方法。注意declare后面有global关键字。

// 即使没有导入prototype_declaration也没有问题
import './prototype_declaration'

function testPrototype() {
    const a = '123'
    const b = a.convertSheep()
    console.log(b)
}

export default testPrototype

即使没有显式导入,ts也能识别到string有这个方法。但要注意,你总得在程序的入口处import这个prototype_declaration,否则运行时找不到convertSheep的实现

8.6 类型推导收窄

function testTypeNarrow1(){
    //推导出来的是数组,(number|string)[]
    let a  = [1,"23"]
    console.log(a)
}

function testTypeNarrow2(){
    //推导出来的是元组,不过是readonly的。[number,string]
    let a  = [1,"23"] as const
    console.log(a)
}

//使用模板进行类型推导时,类型会尽可能收窄
//T用不定参数的方式传入
function tunple<T extends unknown[]>(...ts:T):T{
    return ts
}

function testTypeNarrow3(){
    //推导出来的是元组,不是readonly的,[number,string]
    let a  = tunple(1,"23")
    console.log(a)
}

export default function testTypeNarrow(){
    testTypeNarrow1()
    testTypeNarrow2()
    testTypeNarrow3()
}

let声明的类型,会被类型扩宽。用const声明的类型,会进行类型收窄。使用模板推导的类型,会进行类型收窄。

9 集合与映射类型

代码在这里

9.1 对象模拟map

type User = {
    id: number
    name: string
}
type UserMap = {
    [key in number]: User
}

function TestObjectMockMap1() {
    const data: UserMap = {}

    // key指定了为number类型,以下这句报错
    /*
    data.fish = {
        id: 1,
        name: 'fish',
    }
    */

    console.log('object mock test....')

    data[1] = {
        id: 1,
        name: 'fish',
    }

    data[2] = {
        id: 2,
        name: 'cat',
    }

    console.log(data[1])
    console.log(data.hasOwnProperty(1))
    console.log(data.hasOwnProperty(12))
}

/*
//你不用创建一个以对象为key的map
type UserMap2 = {
    [key in User]: number
}
*/

function TestObjectMockMap2() {
    console.log('nothing happen')
}

export default function TestObjectMockMap() {
    TestObjectMockMap1()
    TestObjectMockMap2()
}

使用key in number或者key in string的方式,我们可以模拟一个map,但是这样做并不安全,因为Object类型本身有prototype,对于prototype上的属性,hasOwnProperty会返回false。

9.2 Set

function TestSet1() {
    console.log('set test1....')
    const a = new Set<number>()

    a.add(1)
    a.add(2)
    console.log(a.has(1)) // true
    console.log(a.has(12)) // false
}

type Country = {
    name: string
}

function TestSet2() {
    console.log('set test2....')
    const data = new Set<Country>()
    data.add({
        name: 'CHINA',
    })
    data.add({
        name: 'CHINA',
    })

    // size为2,不是1,因为js的Set是以对象引用放入的,进行的是浅比较
    console.log(data.size)

    const data2 = new Set<Country>()
    const country = {
        name: 'US',
    }
    data2.add(country)
    data2.add(country)

    // size为1
    console.log(data2.size)
}
export default function TestSet() {
    TestSet1()

    TestSet2()
}

使用ES6的Set类型更为安全,注意,当key类型是对象的时候,它是用引用比较,而不是值比较,这点跟golang不同。当然,为了代码的可读性,我们一般都尽量避免这样做,只对key使用 number与string这种基础类型

9.3 Map

type Car = {
    name: string
    brand: string
}

function TestMap1() {
    console.log('map test1....')
    const a = new Map<number, Car>()

    a.set(1, {
        name: 'S600',
        brand: 'BENZ',
    })
    a.set(2, {
        name: 'RAV4',
        brand: 'TOYOTA',
    })
    console.log(a.get(1)) // 返回S600
    console.log(a.get(12)) // 没有的话,返回undefined
    console.log(a.has(1)) // true
    console.log(a.has(12)) // false
}

function TestMap2() {
    console.log('map test2....')
    const a = new Map<Car, number>()

    a.set(
        {
            name: 'S600',
            brand: 'BENZ',
        },
        1
    )
    a.set(
        {
            name: 'S600',
            brand: 'BENZ',
        },
        1
    )
    // size为2,也是一样,浅比较
    console.log(a.size)

    const b = new Map<Car, number>()
    const car = {
        name: 'RAV4',
        brand: 'TOYOTA',
    }
    b.set(car, 1)
    b.set(car, 2)
    // size为1,浅比较
    console.log(b.size)
}

export default function TestMap() {
    TestMap1()
    TestMap2()
}

使用ES6的Map类型也不错,这个用法与Set很相似了,没啥好说的

10 坑

10.1 关键字命名

代码在这里

import styles from './index.less';

function Creator(){
  const String = (props)=>{
    return (<div>{"String"}</div>);
  }
  const Number = (props)=>{
    return (<div>{"Number"}</div>);
  }
  //这是因为生成代码中,也用到了原生的Object对象,而本地定义的变量也用Object命名,撞在一起了
  //解决方法很简单,永远不要以Object,Array,String,Number这些作为变量命名了,真的太傻了
  /*
  var Object = props => {
    return Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__["jsxDEV"])("div", {
      children: "Object"
    }, void 0, false, {
      fileName: _jsxFileName,
      lineNumber: 11,
      columnNumber: 13
    }, this);
  };
  */
  const Object = (props)=>{
    return (<div>{"Object"}</div>);
  }
  const Array = (props)=>{
    return (<div>{"Array"}</div>);
  }
  return {
    String:String,
    Number:Number,
    Object:Object,
    Array:Array,
  };
}

//以下的代码编译时没有问题,但是运行时会报出Maximum call stack size exceeded错误
const creator = Creator();
export default function IndexPage() {
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
      <div>
        <creator.Array/>
        <creator.Object/>
        <creator.String/>
        <creator.Number/>
      </div>
    </div>
  );
}

永远不要以Object,Array,String,Number这些作为变量命名了,真的太傻了

11 总结

既要保持js语言的动态灵活性,又要有编译时的类型检查。TypeScript可以说是带着手铐在跳舞,因此它开启了类型的编译时运算系统,相当有意思。

更多的Demo看这里

相关文章