1. 1. 类型基础
    1. 1.1. 强类型语言与弱类型语言
      1. 1.1.1. 什么是强类型语言?
      2. 1.1.2. 什么是弱类型语言?
      3. 1.1.3. 小结
    2. 1.2. 静态类型语言与动态类型语言
      1. 1.2.1. JavaScript 与 C++ 对比
      2. 1.2.2. 对比总结
      3. 1.2.3. 其他定义
    3. 1.3. 小结
      1. 1.3.1. 语言类型象限
  2. 2. 基本类型
    1. 2.1. 创建 TypeScript 项目
      1. 2.1.1. 配置构建工具
        1. 2.1.1.1. 公共环境配置 webpack.base.config.js
        2. 2.1.1.2. 开发环境 webpack.dev.config.js
        3. 2.1.1.3. 生产环境 webpack.pro.config.js
        4. 2.1.1.4. 入口文件 webpack.config.js
      2. 2.1.2. 修改 NPM 脚本
        1. 2.1.2.1. 开发环境脚本
        2. 2.1.2.2. 生产环境脚本
    2. 2.2. 基本类型
      1. 2.2.1. 类型注解
      2. 2.2.2. 基本数据类型
  3. 3. 枚举
  4. 4. 类与接口
    1. 4.1. 对象类型接口
      1. 4.1.1. 可索引接口
    2. 4.2. 函数类型接口
    3. 4.3. 混合类型接口
  5. 5. 函数
    1. 5.1. 函数定义
    2. 5.2. 可选参数
    3. 5.3. 参数默认值
    4. 5.4. 剩余参数
    5. 5.5. 函数重载
  6. 6.
    1. 6.1. 继承
    2. 6.2. 成员修饰符
      1. 6.2.1. public
      2. 6.2.2. private
      3. 6.2.3. protected
      4. 6.2.4. readonly
      5. 6.2.5. 构造函数参数
      6. 6.2.6. static
    3. 6.3. 抽象类
    4. 6.4. 多态
    5. 6.5. this 类型
  7. 7. 接口与类的关系
    1. 7.1. 类类型接口
    2. 7.2. 接口的继承
    3. 7.3. 接口继承类
    4. 7.4. 图解
  8. 8. 泛型
    1. 8.1. 概念
    2. 8.2. 泛型函数与泛型接口
    3. 8.3. 泛型类与泛型约束
    4. 8.4. 好处
  • TypeScript 进阶
    1. 1. 类型检查机制
      1. 1.1. 类型推断
        1. 1.1.1. 基础类型推断
        2. 1.1.2. 最佳通用类型推断
        3. 1.1.3. 上下文类型推断
        4. 1.1.4. 类型断言
      2. 1.2. 类型兼容性
        1. 1.2.1. 什么是兼容性
        2. 1.2.2. 接口间兼容
        3. 1.2.3. 函数间兼容
        4. 1.2.4. 枚举兼容性
        5. 1.2.5. 类兼容性
        6. 1.2.6. 泛型兼容性
        7. 1.2.7. 口诀
      3. 1.3. 类型保护
        1. 1.3.1. 四种创建区块的方法
    2. 2. 高级类型
      1. 2.1. 交叉类型
      2. 2.2. 联合类型
        1. 2.2.1. 可区分的联合类型
      3. 2.3. 索引类型
      4. 2.4. 映射类型
      5. 2.5. 条件类型
  • TypeScript 整理手册

    JS 是一门动态弱类型语言,与之对应,自然有静态类型语言和强类型语言。如何区分它们,是在学习一门计算机语言之前首先要搞清楚的问题,理解了这些概念,你就会明白为什么有语言鄙视链,也会对 TS 的诞生的原因有一定的认识。

    类型基础

    强类型语言与弱类型语言

    什么是强类型语言?

    人们对这个概念有不同的理解与定义。

    比较早的定义,在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用函数中声明的类型兼容。

    1
    2
    3
    4
    5
    6
    A() {
    B(x)
    }
    b(y) {
    // y 可以被赋值 想、,运行良好
    }

    这是比较早的定义,约束不是那么严格,后面我们一般这么定义:

    强类型语言:不允许改变变量的数据类型,除非进行强制类型转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class C {
    public static void main(String[] args) {
    int x = 1;
    boolean y = true;
    x = y;
    System.out.println(x);
    // incompatible types: boolean cannot be converted to int
    }
    }

    这样代码会报错,因为两者类型不同:

    1
    2
    3
    4
    // x = y;
    char z = 'a';
    x = z;
    System.out.println(x); // 97

    这里 java 进行了强制类型转换,这样 x 就保持了整形

    什么是弱类型语言?

    弱类型语言:变量可以被赋予不同的数据类型

    1
    2
    3
    4
    let x = 1
    let y = true
    x = y
    console.log(x) // true

    我们可以看到 x 变成了 true

    1
    2
    3
    4
    5
    let x = 1
    let y = true
    let z = 'a'
    x = z
    console.log(x) // a

    这里 x 又变成了字符串

    从这里可以看出 JS 是一门弱类型语言

    小结

    强类型语言中,变量类型转换有严格限制,不同类型的变量还不能相互赋值的,这样就能避免很多类型造成的错误。弱类型语言则没什么约束,虽然运用更灵活,但是更容易出现错误。


    静态类型语言与动态类型语言

    静态类型语言:在编译阶段确定所有变量的类型

    动态类型语言:在执行阶段确定所有变量的类型

    image-20200629102410028

    JS 编译器看到这段代码时,无法确定 ab 的类型,只有程序执行时根据传进来的对象才能确定。

    C++ 在编译的时候就能确定变量的类型了。

    JavaScript 与 C++ 对比

    image-20200629102639343

    由此看出,动态类型语言,无论是在时间还是空间上都有较多的损耗。

    对比总结

    image-20200629102833392

    其他定义

    强类型语言:不允许程序在发生错误后继续执行

    争议:C/C++ 就不是上面所说的强类型语言,没有对数组越界进行检查


    小结

    语言类型象限

    image-20200629103153332

    问题:TypeScript 是什么类型语言?

    TS是强类型,面向对象,编译型的语言。

    基本类型

    正式开始 TypeScript 程序编写(Helloworld)

    准备一个 ts 目录然后用 vscode 编辑他:

    image-20200629104418006

    创建 TypeScript 项目

    1. 打开控制台,创建 package.json
    1
    npm init -y    
    1. 安装 TypeScript(全局)
    1
    npm i typescript -g
    1. 查看 tsc 帮助信息验证安装
    1
    tsc -h

    我们可以看到很多配置项目,这些项目可以通过一个配置文件来实现

    1. 创建配置项 tsconfig.json
    1
    tsc --init

    这个文件有完整的注释

    1. 新建目录创建文件
    1
    2
    3
    mkdir src
    cd scr
    touch index.ts
    1. 编写文件
    1
    let hello : string = 'hello typescript'
    1. 编译这个文件
    1
    tsc ./src/index.ts

    我们可以看到一个新的 JavaScript 文件输出了,看一下它的内容

    1
    var hello = 'hello typescript';

    除此之外我们还可以通过官网的 playground 来查看编译结果:

    image-20200629105743889

    输出的结果和 tsc 是一样的

    配置构建工具

    使用 webpack 管理我们的工程(三个包):

    1
    npm i webpack webpack-cli webpack-dev-server -D

    配置 webpack 的时候,需要区分开发环境和生产环境,因为两个环境是不一样的,需要做不一样的配置。为了配置的可维护性,我们可以将开发环境的配置,生产环境的配置还有公共配置分开书写,最后通过插件进行合并。

    • webpack.config.js:所有文件的入口
    • webpack.base.config.js:公共环境的配置
    • webpack.dev.config.js :开发环境的配置
    • webpack.pro.config.js :生产环境的配置
    公共环境配置 webpack.base.config.js
    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
    const HtmlWebpackPlugin = require('html-webpack-plugin')

    module.exports = {
    entry: './src/index.ts',
    output: {
    filename: 'app.js'
    },
    resolve: {
    extensions: ['.js', '.ts', '.tsx']
    },
    module: {
    rules: [
    {
    test: /\.tsx?$/i,
    use: [{
    loader: 'ts-loader'
    }],
    exclude: /node_modules/
    }
    ]
    },
    plugins: [
    new HtmlWebpackPlugin({
    template: './src/tpl/index.html'
    })
    ]
    }

    • 首先指定了一个入口文件 ./src/index.ts

    • 然后输出文件名是 app.js,使用默认的根目录

    • 使用三个扩展名 .js .ts .tsx

    • 既然我们引入了新的文件 ts,那就需要使用相应的 loader,我们安装 ts-loader

      1
      npm i ts-loader typescript -D // 还需要本地安装一下 typescript
    • ts-loader 的正则:/\.tsx?$/i,并排除 node_module 下的文件

    • 插件 HtmlWebpackPlugin:通过一个模板帮助生成网站首页,而且帮助把输出文件自动嵌入到模板文件中,安装一下

      1
      npm i html-webpack-plugin -D
    • 编写一下模板文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // ./src/tpl/index.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>TypeScript</title>
      </head>
      <body>
      <div class="app"></div>
      </body>
      </html>
    开发环境 webpack.dev.config.js
    1
    2
    3
    module.exports = {
    devtool: 'cheap-module-eval-source-map'
    }

    这是官方推荐的配置,

    1. cheap 表示忽略文件的列信息(调试这个没用)
    2. module 表示定位到 TypeScript 源码,而不是经 loader 转义的 JavaScript 源码
    3. eval-source-map 表示会将 source-mapdataUrl 的形式打包到文件中,重编译速度是很快的,不用担心性能问题。
    生产环境 webpack.pro.config.js
    1
    2
    3
    4
    5
    6
    7
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')

    module.exports = {
    plugins: [
    new CleanWebpackPlugin()
    ]
    }

    这个作用是,在每次成功构建之后,帮助我们清空 dist 目录。因为有的时候为了避免缓存,会在文件后加入哈希,在多次构建后会产生很多无用文件。这个插件会帮我们清空目录。我们安装一下:

    1
    npm i clean-webpack-plugin -D
    入口文件 webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const merge = require('webpack-merge')
    const baseConfig = require('./webpack.base.config')
    const devConfig = require('./webpack.dev.config')
    const proConfig = require('./webpack.pro.config')

    module.exports = (env, argv) => {
    let config = argv.mode === 'development' ? devConfig : proConfig;
    return merge(baseConfig, config);
    };

    使用了一个合并插件,我们安装一下:

    1
    npm i webpack-merge -D

    修改 NPM 脚本

    开发环境脚本
    1
    2
    3
    4
    5
    6
    7
    // package.json

    "main": "./src/index.ts",
    "scripts": {
    "start": "webpack-dev-server --mode=development --config ./build/webpack.config",
    "test": "echo \"Error: no test specified\" && exit 1"
    },

    运行一下 npm start,我们可以看到效果:

    image-20200629120625940

    image-20200629120634351

    可以在模板 html 中写一些东西:

    1
    <div class="app">123123</div>

    然后就能在 http://127.0.0.1:8080 中看到

    image-20200629120815821

    生产环境脚本
    1
    2
    3
    4
    5
    6
    7
    // package.json

    "scripts": {
    "build": "webpack --mode=produnction --config ./build/webpack.config.js",
    "start": "webpack-dev-server --mode=development --config ./build/webpack.config",
    "test": "echo \"Error: no test specified\" && exit 1"
    },

    运行一下 npm run build

    image-20200629121451340

    我们能看在目录中看到 dist,构建好的 app.js 也嵌入到模板文件中。


    基本类型

    image-20200629104030469

    ES6 有 6 种基本数据类型,还有 3 种引用类型,TypeScript 又新增了几个类型。

    类型注解

    作用:相当于强类型语言的类型声明

    语法:(变量/函数):type

    基本数据类型

    到创建好的工程目录去,在 src 下创建 datatype.ts 文件,并引入到 index.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
    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
    // ./src/datatype.ts

    // 原始类型
    let bool: boolean = true
    let num: number = 123
    let str: string = 'abc'

    // 数组
    let arr1: number[] = [1, 2, 3]
    let arr2: Array<number> = [1, 2, 3]
    let arr3: Array<number | string> = [1, 2, 3, '4']

    // 元祖
    let tuple: [number, string] = [0, '1']
    // 元祖越界问题
    tuple.push(2)
    console.log(tuple) // 打印三个元素
    // tuple[2] // 不允许越界访问

    // 函数
    // let add = (x, y) => x + y // 需要类型注解
    let add = (x: number, y: number): number => x + y
    // 返回值可以省略:TS 的推断
    // 没有具体实现的函数类型
    let compute: (x: number, y: number) => number
    // 具体实现它
    compute = (a, b) => a + b

    // 对象
    // let obj: object = {x: 1, y: 3} // 简单指定类型,没有定义属性
    let obj: {x: number, y: number} = {x: 1, y: 3}
    obj.x = 3

    // Symbol
    let s1: symbol = Symbol()
    let s2 = Symbol()
    console.log(s1 === s2) // false

    // undefined, null
    // 不能赋值其他数据类型
    let un: undefined = undefined
    let nu: null = null
    // 别的数据类型可以被赋值,需要设置 tsconfig
    // "strictNullChecks": false
    // let num: number | undefined | null = 123

    // void
    let noReturn = () => {}
    // js 中 void 是一种操作符,可以让任何表达式返回 undefined
    // void 0 // 因为 undefined 不是一个保留字,可以被修改覆盖
    // ts 中 void 是一个没有任何返回值的类型

    // any
    let x
    // ts 中不指定一个变量类型,默认就是 any

    // never
    let error = () => {
    throw new Error('error')
    }
    let endless = () => {
    while(true) {}
    }
    // 永远不会有返回值的类型:函数抛出异常,死循环函数

    TypeScript 完全覆盖了 ES6 的基本数据类型,并通过 any 实现了对 JS 的兼容


    枚举

    一个角色判断的例子:

    image-20200629125343475

    枚举:一组有名字的常量集合

    image-20200629125446183

    接着之前的工程,新建 enum.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
    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
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    // 数字枚举
    enum Role {
    Reporter,
    Developer,
    Maintainer,
    Owner,
    Guest
    }
    console.log(Role.Reporter) // 0
    console.log(Role.Developer) // 1 递增
    console.log(Role) // 编译成一个对象
    // 自定义初始值
    // enum Role {
    // Reporter,
    // Developer,
    // Maintainer,
    // Owner,
    // Guest
    // }
    // "use strict";
    // var Role;
    // (function (Role) {
    // Role[Role["Reporter"] = 0] = "Reporter";
    // Role[Role["Developer"] = 1] = "Developer";
    // Role[Role["Maintainer"] = 2] = "Maintainer";
    // Role[Role["Owner"] = 3] = "Owner";
    // Role[Role["Guest"] = 4] = "Guest";
    // })(Role || (Role = {}));
    // 反向映射,枚举的实现原理

    // 字符串枚举
    enum Message {
    Success = '成功',
    Fail = '失败'
    }
    // 不可以反向映射

    // 异构枚举
    enum Answer {
    N,
    Y = 'yes'
    }
    // 容易引起混淆,不建议使用

    // 枚举成员性质
    // Role.Reporter = 2 // 只读类型


    // 枚举成员
    enum Char {
    // const 常量枚举(没有初始值,对成员引用,常量表达式)
    // 编译阶段出结果
    a,
    b = Char.a,
    c = 1 + 3,
    // computed 需要被计算
    // 执行阶段出结果
    d = Math.random(),
    e = '123'.length,
    // computed 后面的成员需要初始值
    f = 4
    }
    // var Char;
    // (function (Char) {
    // Char[Char["a"] = 0] = "a";
    // Char[Char["b"] = 0] = "b";
    // Char[Char["c"] = 4] = "c";
    // Char[Char["d"] = Math.random()] = "d";
    // Char[Char["e"] = '123'.length] = "e";
    // })(Char || (Char = {}));

    // 常量枚举
    // 编译阶段移除
    const enum Month {
    Jan,
    Feb,
    Mar
    }
    // 当我们不需要一个对象,而需要对象的值
    let month = [Month.Jan, Month.Feb] // 值直接替换成常量

    // 枚举类型
    // 三种情况下,枚举和枚举成员都可以作为单独类型存在
    enum E { a, b }
    enum F { a = 0, b = 1 }
    enum G { a= 'apple', b = 'banana' }

    let e: E = 3
    let f: F = 3
    // e === f // 报错

    let e1: E.a
    let e2: E.b
    let e3: E.a = 1
    // e1 === e2 // 报错
    // e1 === e3 // 可以比较

    // 字符串枚举取值只能是枚举成员的类型
    let g1: G = G.b
    let g2: G.a = G.a // 只能这样赋值

    枚举:帮助我们将程序中不容易记忆的硬编码或者是未来中可能改变的常量抽取出来,提高程序可读性、可维护性


    类与接口

    对象类型接口

    接口可以约束对象,函数以及类的结构和类型,这是一种代码协作的契约,我们必须遵守并且不能改变。我们来看一个对象类型的接口是怎么定义的:

    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
    // interface.ts

    interface List {
    id: number,
    name: string
    }

    interface Result {
    data: List[]
    }

    function render(result: Result) {
    result.data.forEach((val) => {
    console.log(val.id, val.name)
    })
    }

    let result = {
    data: [
    { id: 1, name: 'A' },
    { id: 2, name: 'B' }
    ]
    }

    render(result)

    实际开发中,我们经常会接收到后端传递过来的意料之外的字段,会发生什么呢?

    1
    2
    3
    4
    5
    6
    let result = {
    data: [
    { id: 1, name: 'A', sex: 'male' },
    { id: 2, name: 'B' }
    ]
    }

    这时候没有报错,因为鸭式变形法:看上去是,那他就是。换到 TS 上就是,只要传入的对象满足接口的必要条件,那么它就被允许,即使传入多余字段。

    有一个例外,如果我们直接传入对象自变量,它就会对额外字段进行类型检查:

    1
    2
    3
    4
    5
    6
    7
    render( {
    data: [
    { id: 1, name: 'A', sex: 'male' },
    { id: 2, name: 'B' }
    ]
    })
    // 报错

    三种解决方法:

    1. 对象自变量赋值给变量

      1
      render(result)
    2. 类型断言

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      render( {
      data: [
      { id: 1, name: 'A', sex: 'male' },
      { id: 2, name: 'B' }
      ]
      } as Result)

      // React 中不建议使用
      render(<Result> {
      data: [
      { id: 1, name: 'A', sex: 'male' },
      { id: 2, name: 'B' }
      ]
      } as Result)
    3. 字符串索引签名

    1
    2
    3
    4
    5
    interface List {
    id: number,
    name: string,
    [x: string]: any
    }

    用任意字符串索引 List

    接口成员的一些属性:

    可选

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface List {
    id: number,
    name: string,
    [x: string]: any,
    age?: number
    }
    ...
    function render(result: Result) {
    result.data.forEach((val) => {
    console.log(val.id, val.name)
    if(val.age) {
    console.log(val.age)
    }
    })
    }

    只读

    1
    2
    3
    4
    5
    6
    interface List {
    readonly id: number,
    name: string,
    [x: string]: any,
    age?: number
    }

    可索引接口

    当你不确定一个接口中有多少个属性时,就可以使用可索引类型的接口,可索引类型的接口可以用数字索引,也可以是字符串。

    1
    2
    3
    4
    5
    interface StringArray {
    [index: number]: string
    }

    let chars: StringArray = ['A', 'B']

    用任意数字索引 StringArray 都会得到一个 string,相当于声明了一个字符串数组

    1
    2
    3
    4
    interface Names {
    [index: string]: string,
    // x: number 不可以
    }

    用任意 string 索引,得到的都是 string,而且不能再声明 number 类型成员

    1
    2
    3
    4
    5
    interface Names {
    [index: string]: string,
    // x: number 不可以
    [z: number]: string
    }

    索引可以混用,需要注意的是,数字索引签名返回值一定要是字符串索引签名返回值的子类型,因为 JavaScript 会类型转换,将 number 转换为 string

    函数类型接口

    使用接口定义一个函数,之前用变量声明了一个函数类型,现在我们用接口实现一遍:

    1
    2
    3
    4
    5
    let add: (x: number, y: number) => number

    interface Add {
    (x: number, y: number): number
    }

    两种方式等价,还有一种更简洁的方式:

    1
    type Add = {x: number, y: number} => number

    这是使用类型别名来声明函数类型

    1
    2
    3
    type Add = (x: number, y: number) => number

    let add: Add = (a, b) => a + b

    混合类型接口

    一个接口既可以定义一个函数,又可以像对象一样,拥有属性和方法

    1
    2
    3
    4
    5
    interface Lib {
    (): void,
    version: string,
    doSomething(): void
    }

    定义完成后我们来实现它:

    1
    2
    3
    let lib: Lib = () => {}
    lib.version = '1.0'
    lib.doSomething = () => {}

    还是报错,这时候就需要使用断言:

    1
    2
    3
    let lib: Lib = (() => {}) as Lib
    lib.version = '1.0'
    lib.doSomething = () => {}

    实现了一个混合接口,但是向全局暴露了一个变量 lib,它是一个单例,如果我们创建了多个 lib,就需要封装一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function getLib() {
    let lib: Lib = (() => {}) as Lib
    lib.version = '1.0'
    lib.doSomething = () => {}
    return lib
    }

    let lib1 = getLib()
    lib1()
    lib1.doSomething

    let lib2 = getLib()

    接口还可以定义类的结构与类型,之后再讲

    函数

    函数定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function add1(x: number, y: number) {
    return x + y
    }

    let add2: (x: number, y: number) => number

    type add3 = (x: number, y: number) => number

    interface add4 {
    (x: number, y: number): number
    }

    我们需要明确指出函数参数的类型与个数,而返回值可以使用 ts 的类型推断。后三种只是函数类型的定义(函数声明),缺少函数体。

    可选参数

    1
    2
    3
    4
    function add5(x: number, y?: number) {
    return y? x + y : x
    }
    add5(1)

    要注意的是,可选参数不能在必选参数之前。

    参数默认值

    类似 ES6 的形式

    1
    2
    3
    function add6(x: number, y = 2, z: number, q = 1) {
    return x + y + z + q
    }

    需要注意的是,在必选参数之前,默认参数是不可以省略的,必须明确传入 undefined

    1
    add6(1, undefined, 3)

    剩余参数

    剩余参数是以数组形式存储的:

    1
    2
    3
    function add7(x: number, ...rest: number[]) {
    return x + rest.reduce((p, c) => p + c )
    }

    函数重载

    静态类型语言中都有这个概念,两个函数如果名称相同,但是参数个数与参数类型不同,那么就实现了函数重载,增强了函数的可读性与可扩展性

    TS 的函数重载要求我们先定义一系列名称相同的函数声明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function add8(...rest: number[]): number
    function add8(...rest: string[]): string
    function add8(...rest: any[]): any {
    let first = rest[0]
    if(typeof first === 'string') {
    return rest.join('')
    }
    if(typeof first === 'number') {
    return rest.reduce((p, c) => p + c)
    }
    }
    add8(1, 2, 3)
    add8('a', 'b', 'c')

    TS 在重载时会查找重载列表,,所以我们要将容易匹配的函数定义写在前面

    ES6 引入了 class 关键字,我们终于可以像后端语言一样实现面向对象了,TS 的类覆盖了 ES6 的类,并且引入了一些其他特性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Dog {
    constructor(name: string) {
    this.name = name
    }
    name: string
    run() {}
    }
    console.log(Dog.prototype)
    let dog = new Dog('wangcai')
    console.log(dog)

    //-----------------------------
    class Dog {
    constructor() {
    this.name = 'wangcai'
    }
    name: string
    run() {}
    }
    console.log(Dog.prototype)
    let dog = new Dog()
    console.log(dog)

    与 ES6 不同的是,我们添加了类型注解,此外还有一个 run() 方法,返回值是 void,还有一个构造函数的返回值,返回 Dog 本身(自动推断),注意两个问题:

    1. 无论在 ES 还是 TS 中,类成员的属性都是实例属性而不是原型属性,类成员方法都是实例方法
    2. 与 ES 不同的是,实例属性必须要有初始值或者在构造函数中被初始化

    继承

    1
    2
    3
    4
    5
    6
    7
    class Husky extends Dog {
    constructor(name: string, color: string) {
    super(name)
    this.color = color
    }
    color: string
    }

    ES 规定派生类的构造函数必须包含 super() 调用,代表父类实例,this 需要放在构造函数之后

    成员修饰符

    这是 TS 对 ES 的一种扩展

    public

    默认修饰符,对所有人可见

    1
    public name: string = 'dog'

    private

    只能在类的本身被调用,实例都不行

    1
    private pri() {}

    可以给构造函数加上私有成员属性,作用是这个类既不能被实例化,也不能被继承

    protected

    受保护成员只能在类或者子类中访问,而不能在实例中访问

    1
    protected pro() {}

    可以给构造函数加上受保护属性,作用是这个类只能被继承,也就是实现了一个基类

    readonly

    只读属性不可以被更改,只读属性一定要被初始化,和实例属性一样的

    1
    readonly legs: number = 4

    构造函数参数

    将参数变为了实例属性,这样我们就不用在类中定义了,更简洁

    1
    2
    3
    4
    5
    6
    7
    class Husky extends Dog {
    constructor(name: string, public color: string) {
    super(name)
    this.color = color
    }
    // color: string
    }

    static

    类的静态成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Dog {
    constructor(name: string) {
    this.name = name
    }
    name: string
    run() {}
    static food: string = 'bones'
    }

    静态成员只能通过类名来调用,不能通过实例或者子类调用

    抽象类

    ES 中并没有引入抽象类的概念,所谓抽象类就是只能被继承,不能被实例化的类

    1
    2
    3
    4
    abstract class Animal {

    }
    // let animal = new Animal

    抽象类中可以定义具体的方法,子类就不用实现了,达成了方法的复用

    抽象类中可以不完成一个方法的具体实现,抽象方法:

    1
    abstract sleep(): void

    抽象类可以抽离出类的共性,从而提高代码的复用率

    多态

    父类中定义一个抽象方法,多个子类中对这个方法有不同的实现,实现运行时的绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Dog extends Animal {
    sleep() {
    console.log('dog sleep')
    }
    }

    class Cat extends Animal {
    sleep() {
    console.log('cat sleep')
    }
    }
    let dog = new Dog()
    let cat = new Cat()
    let animals: Animal[] = [dog, cat]
    animals.forEach(i => {
    i.sleep()
    })

    this 类型

    特殊的 TS 类型,类的成员方法可以直接返回一个 this ,这样就可以很方便地实现链式调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class WorkFlow {
    step1() {
    return this
    }
    step2() {
    return this
    }
    }
    new WorkFlow().step1().step2()

    这样就实现了 TS 类方法的链式调用

    this 的多态

    this 既可以是父类型,也可以是子类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class WorkFlow {
    step1() {
    return this
    }
    step2() {
    return this
    }
    }
    new WorkFlow().step1().step2()

    class MyFlow extends WorkFlow {
    next() {
    return this
    }
    }
    new MyFlow().next().step1().next().step2()

    这样就保持了父类与子类间接口调用的连贯性


    接口与类的关系

    类类型接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface Human {
    name: string,
    eat(): void
    }

    class Asian implements Human {
    constructor(name: string) {
    this.name = name
    }
    name: string
    eat() {}
    }

    一个接口可以约束类有多少属性以及它们的类型,注意点:

    • 类实现接口的时候,必须实现接口中所有属性(类可以添加属性)
    • 接口只能约束类的公有成员
    • 接口不能约束类的构造函数

    接口的继承

    接口可以像类一样实现继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    interface Human {
    name: string,
    eat(): void
    }

    interface Man extends Human {
    run(): void
    }

    interface Child {
    cry(): void
    }

    interface Boy extends Man, Child {}

    let boy: Boy = {
    name: '',
    run() {},
    eat() {},
    cry() {}
    }

    接口继承类

    相当于把类的成员都抽象了出来,也就是只有结构,没有实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Auto {
    state = 1
    }

    interface AutoInterface extends Auto {
    // 隐含了 state 属性
    }

    class C implements AutoInterface {
    state = 2
    }

    class Bus extends Auto implements AutoInterface {
    // 子类已经有 state 了
    }

    接口不仅抽象了公共属性,而且把私有成员和受保护成员都抽象出来了

    图解

    image-20200629155610543


    泛型

    希望一个函数或者一个类支持多种数据类型,怎么解决呢?

    概念

    image-20200629160048424

    我们希望接收字符串数组,那么可以使用函数重载来实现:

    image-20200629160138362

    还可以用联合类型,更简便一些:

    image-20200629160212744

    我们还不满足,希望接收任何类型参数,any 类型:

    image-20200629160252817

    但是这样会丢失一些约束信息,也就是类型之间的约束关系,忽略了输入参数与返回值的类型必须一致。这个时候就用到泛型了。

    泛型概念:不预先确定的数据类型,具体的类型在使用的时候才能确定

    泛型函数与泛型接口

    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
    function log<T>(value: T): T {
    console.log(value)
    return value
    }
    log<string[]>(['a', 'b'])
    // 类型推断,推荐
    log(['a', 'b'])

    // 泛型别名
    type Log = <T>(value: T) => T
    let myLog: Log = log

    // 泛型接口
    interface Log2 {
    <T>(value: T): T
    }

    // 泛型约束接口所有成员,实现指定类型
    interface Log3<T> {
    (value: T): T
    }
    let myLog3: Log3<number> = log
    myLog3(1)

    interface Log4<T = string> {
    (value: T): T
    }

    将泛型变量与函数参数等同对待,只不过是代表值得参数,泛型在高阶函数中有广泛应用。

    泛型类与泛型约束

    泛型类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 泛型不能应用于类的静态成员
    class Log<T> {
    run(value: T) {
    console.log(value)
    return value
    }
    }
    let log1 = new Log<number>()
    log1.run(1)
    let log2 = new Log()
    log2.run({a:1}) // 任意值

    泛型约束

    1
    2
    3
    4
    5
    function log<T>(value: T): T {
    console.log(value, value.length)
    return value
    }
    // 报错,不存在 length

    预定义接口,进行约束:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface Length {
    length: number
    }

    function log<T extends Length>(value: T): T {
    console.log(value, value.length)
    return value
    }

    不管传入什么,必须要有 length 属性(数组、字符串、集合)

    好处

    • 增强程序的可扩展性:函数或类可以轻松支持多种数据类型
    • 增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明
    • 灵活控制类型之间的约束

    到此,基础知识基本介绍结束

    TypeScript 进阶

    类型检查机制

    类型检查机制:TypeScript 编译器在做类型检查时,秉承的一些原则,以及表现出的一些行为

    作用:辅助开发,提高效率

    • 类型推断
    • 类型兼容性
    • 类型保护

    类型推断

    不用指定变量的类型(函数的返回值类型),TypeScript 可以根据某些规则自动为其推断一个类型

    • 基础类型推断
    • 最佳通用类型推断
    • 上下文类型推断

    基础类型推断

    1
    2
    3
    4
    let a = 1
    let b = [1]

    let c = (x = 1) => x + 1

    最佳通用类型推断

    需要从多个类型中推断类型时,TS 会尽可能推断一个兼容所有类型的,通用的类型

    1
    2
    // number || null
    let b = [1, null]

    上下文类型推断

    上面两个都是从右到左的推断,上下文是从左到右的推断,通常发生在事件处理中

    1
    2
    3
    4
    window.onkeydown = (event) => {
    // KeyboardEvent
    console.log(event) // 键盘事件
    }

    类型断言

    以上三个方法可能不符合你的预期,TS 也提供了一个方法供你覆盖推断类型

    1
    2
    let foo = {}
    foo.bar = 1 // 报错
    1
    2
    3
    4
    5
    6
    interface Foo {
    bar: number
    }

    let foo = {} as Foo
    foo.bar = 1 // 报错

    类型断言不能乱用:

    1
    2
    3
    4
    5
    interface Foo {
    bar: number
    }

    let foo = {} as Foo

    这时候没有按照接口约定严格拥有属性,怎么办呢?:

    1
    2
    3
    4
    5
    6
    interface Foo {
    bar: number
    }
    let foo: Foo = {
    bar: 1
    }

    这才是最佳方法


    类型兼容性

    什么是兼容性

    当一个类型 Y 可以赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y

    X 兼容 Y:X(目标类型) = Y(源类型)

    接口间兼容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 接口兼容性
    interface X {
    a: any,
    b: any
    }

    interface Y {
    a: any,
    b: any,
    c: any
    }

    let x: X = { a: 1, b: 2 }
    let y: Y = { a: 1, b: 2, c: 3 }
    x = y // true
    y = x // false

    源类型只要有目标类型的必要属性,就可以进行赋值

    函数间兼容

    常用于函数作为参数回调的场景

    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
    // 函数兼容性
    type Handler = (a: number, b: number) => void
    function hof(handler: Handler) {
    return handler
    }

    // 1) 参数个数
    let handler1 = (a: number) => {}
    hof(handler1) // 可以
    let handler2 = (a: number, b: number, c: number) => {}
    hof(handler2) // 不可以

    // 可选参数 / 剩余参数
    let a = (p1: number, p2: number) => {}
    let b = (p1?: number, p2?: number) => {}
    let c = (...args: number[]) => {}
    // 固定参数是可以兼容可选参数与剩余参数的
    // 可选参数是不兼容固定参数与剩余参数的
    // 剩余参数可以兼容两者

    // 2) 参数类型
    // 类型一定要匹配
    let handler3 = (a: string) => {}
    hof(handler3) // 不可以

    interface Point3D {
    x: number,
    y: number,
    z: number
    }

    interface Point2D {
    x: number,
    y: number
    }

    let p3d = (point: Point3D) => {}
    let p2d = (point: Point2D) => {}
    p3d = p2d // 兼容
    p2d = p3d // 不兼容
    // 函数参数的双向协变,特殊例子

    // 3) 返回值类型
    let f = () => ({name: 'Alice'})
    let g = () => ({name: 'Alice', location: 'shanghai'})
    f = g // 兼容
    g = f // 不兼容


    // 函数重载
    function overload(a: number, b: number): number
    function overload(a: string, b: string): string
    function overload(a: any, b: any): any {}
    // function overload(a: any, b: any) {}
    // function overload(a: any, b: any, c: any): any {}

    枚举兼容性

    1
    2
    3
    4
    5
    6
    7
    8
    // 枚举兼容性
    // 枚举和 number 兼容
    enum Fruit { Apple, Banana }
    enum Color { Red, Blue }
    let fruit: Fruit.Apple = 3
    let no: number = Fruit.Apple // 兼容
    // 枚举之间不兼容
    let color: Color.Red = Fruit.Apple

    类兼容性

    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
    // 类兼容性(与接口类似)
    // 静态成员与构造函数不参与比较
    // 拥有相同实例,实例间兼容
    class A {
    constructor(a: number, b: number) {}
    id: number = 1
    private name: string = ''
    }
    class B {
    static s = 1
    constructor(p: number) {}
    id: number = 2
    private name: string = ''
    }

    let aa = new A(1, 2)
    let ab = new B(1)
    aa = ab
    ab = aa
    // 含有私有成员,只有父类子类兼容
    class D extends A {}
    let ad = new D(1, 2)
    aa = ac
    ac = aa

    泛型兼容性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 泛型兼容性
    interface Empty<T> {

    }
    let o1: Empty<number> = {}
    let o2: Empty<string> = {}
    o1 = o2 // 因为没有任何成员

    interface Empty2<T> {
    value: T
    }
    let o3: Empty2<number> = {1}
    let o4: Empty2<string> = {'a'}
    o3 = o4 // 不兼容

    // 泛型函数
    let loga = <T>(x: T): T => {
    return x
    }
    let logb = <U>(y: U): U => {
    return y
    }
    loga = logb

    口诀

    • 结构之间兼容:成员少的兼容成员多的
    • 函数之间兼容:参数多的兼容参数少的

    类型保护

    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
    enum Type {Strong, Week}

    class Java {
    helloJava() {
    console.log('Hello Java')
    }
    }

    class JavaScript {
    helloJavaScript() {
    console.log('Hello JavScript')
    }
    }

    function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java() : new JavaScript()
    if('helloJava' in lang) {
    lang.helloJava()
    } else {
    lang.helloJavaScript()
    }
    return lang
    }

    getLanguage(Type.Strong)

    TypeScript 能够在特定区块中保证变量属于某确定类型

    可以在此区块中放心地引用此类型的属性,或者调用此类型的方法

    四种创建区块的方法

    1
    2
    3
    4
    5
    6
    // instanceof
    if(lang instanceof Java) {
    lang.helloJava()
    } else {
    lang.helloJavaScript()
    }
    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
    // in
    class Java {
    helloJava() {
    console.log('Hello Java')
    }
    java: any
    }

    class JavaScript {
    helloJavaScript() {
    console.log('Hello JavScript')
    }
    javascript: any
    }

    function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java() : new JavaScript()
    // in
    if('java' in lang) {
    lang.helloJava()
    } else {
    lang.helloJavaScript()
    }
    return lang
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // typeof

    function getLanguage(type: Type, x: string | number) {
    // typeof
    if(typeof x === 'string'){
    x.length
    } else {
    x.toFixed(2)
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 创建类型保护函数
    function isJava(lang: Java | JavaScript): lang is Java {
    return (lang as Java).helloJava !== undefined
    }

    function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java() : new JavaScript()

    if(isJava(lang)) {
    lang.helloJava()
    } else {
    lang.helloJavaScript()
    }
    return lang
    }

    getLanguage(Type.Strong)

    高级类型

    高级类型就是指 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
    interface DogInterface {
    run(): void
    }
    interface CatInterface {
    jump(): void
    }
    let pet: DogInterface & CatInterface = {
    run() {},
    jump() {}
    }

    class Dog implements DogInterface {
    run() {},
    eat() {}
    }

    class Cat implements CatInterface {
    jump() {}
    eat() {}
    }
    enum Master { Boy, Girl }
    function getPet(master: Master) {
    let pet = master === Master.Boy ? new Dog() : new Cat
    pet.eat() // 未确定情况下只能访问交集
    return pet
    }

    我们看到这个就是交叉类型(所有类型的并集):

    1
    2
    3
    4
    let pet: DogInterface & CatInterface = {
    run() {},
    jump() {}
    }

    联合类型

    明确概念:声明的类型并不确定,可以为其中的一个

    1
    2
    3
    4
    let a: number | string = 'a'
    // 限定取值
    let b: 'a' | 'b' | 'c'
    let c: 1 | 2 | 3
    1
    2
    3
    4
    5
    function getPet(master: Master) {
    let pet = master === Master.Boy ? new Dog() : new Cat
    pet.eat() // 未确定情况下只能访问交集
    return pet
    }

    可区分的联合类型

    这种模式本质上讲,是一种结合了联合类型与自身变量的类型保护方法。一个类型如果是多个类型的联合类型,且每个类型之间有一个公共属性,我们就可以借助这个公共属性,创建不同的类型保护区块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    interface Square {
    kind: 'square',
    size: number
    }

    interface Rectangle {
    kind: 'rectangle',
    width: number,
    height: number
    }

    type Shape = Square | Rectangle
    function area(s: Shape) {
    switch(s.kind) {
    case 'square':
    return s.size * s.size
    case 'rectangle':
    return s.width * s.height
    }
    }

    如果我们想要加新的 Shape,就可能有一些隐患:

    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
    interface Square {
    kind: 'square',
    size: number
    }

    interface Rectangle {
    kind: 'rectangle',
    width: number,
    height: number
    }

    interface Circle {
    kind: 'circle',
    r: number
    }

    type Shape = Square | Rectangle | Circle
    function area(s: Shape) {
    switch(s.kind) {
    case 'square':
    return s.size * s.size
    case 'rectangle':
    return s.width * s.height
    }
    }

    我们可以打印:

    1
    console.log(area({kind: 'circle', r: 1})

    返回一个 undefined,这样没有约束是不好的,我们修改一下:

    1
    2
    // 方法1:指定返回值
    function area(s: Shape):number
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 方法2:使用 never 类型
    type Shape = Square | Rectangle | Circle
    function area(s: Shape) {
    switch(s.kind) {
    case 'square':
    return s.size * s.size
    case 'rectangle':
    return s.width * s.height
    case 'circle':
    return s.r * s.r * 3.14 / 2
    default:
    return ((e: never) => { throw new Error(e) })(s)
    }
    }

    索引类型

    常见场景:

    1
    2
    3
    4
    5
    let obj = {
    a: 1,
    b: 2,
    c: 3
    }

    抽取值,形成数组:

    1
    2
    3
    4
    5
    function getValues(obj: any, keys: string[]) {
    return keys.map(key => obj[key])
    }
    console.log(getValues(obj, ['a', 'b'])) // [1, 2]
    console.log(getValues(obj, ['a', 'd'])) // [undefined, undefined]

    但是编译器并没有报错,我们使用索引类型进行约束,我们先要了解几个概念

    • 索引类型的查询操作符 keyof T:表示类型 T 的所有公共属性的自变量的联合类型

      1
      2
      3
      4
      5
      interface Obj {
      a: number,
      b: number
      }
      let key: keyof Obj // 'a' | 'b'
    • 索引访问操作符 T[K]:表示对象 T 的属性 K 所代表的类型

      1
      let value: Obj['a'] // number
    • 泛型继承约束 T extends U:泛型变量通过集成某个类型,获得某些属性

    我们现在来改造函数:

    1
    2
    3
    function getValues<T, K extends keysof T>(obj: T, keys: K[]): T[K][] {
    return keys.map(key => obj[key])
    }

    这时候 TS 类型检查就起作用了

    索引类型能实现对对象属性的查询访问,然后泛型约束,就能建立对象、对象属性及属性值之间的约束关系

    映射类型

    通过映射类型可以从旧的类型生成新的类型,把一个类型中的所有属性变为只读

    1
    2
    3
    4
    5
    6
    7
    interface Obj {
    a: string,
    b: number,
    c: boolean
    }
    // 通过 TS 内置的泛型接口转换
    type ReadonlyObj = Readonly<Obj>

    这是如何实现的呢?我们跳转到实现方法:

    1
    2
    3
    type Readonly<T> = {
    readonly [P in keyof T]: T[P];
    };

    索引签名是 P in keyof T:执行了一次遍历操作,P 依次绑定到 T 的属性上

    索引签名返回值是索引访问操作符 T[P]:属性 P 所指定的类型

    最后加上 readonly,就将所有属性变为了只读

    把一个接口的所有属性变为可选的:

    1
    type PartialObj = Partial<Obj>
    1
    2
    3
    type Partial<T> = {
    [P in keyof T]?: T[P];
    };

    pick 映射类型,可以抽取 obj 类型的子集

    1
    type PickObj = Pick<Obj, 'a' | 'b'>
    1
    2
    3
    type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
    };

    以上三种类型,官方称之为同态,也就是说它们不会引入新的类型

    创建新的属性的映射类型:Record

    1
    type RecordObj = Record<'x' | 'y', Obj>
    1
    2
    3
    type Record<K extends keyof any, T> = {
    [P in K]: T;
    };

    条件类型

    条件类型是一种由条件表达式所决定的类型,形式如下:

    1
    T extends U ? X : Y

    如果类型 T 可以赋值给类型 U,那么结果类型就是 X 类型,条件类型使类型具有了不唯一性,同样的增加了类型的灵活性。

    1
    2
    3
    4
    5
    6
    7
    type TypeName<T> = 
    T extends string ? 'string' :
    T extends number ? 'number' :
    T extends boolean ? 'boolean' :
    T extends undefined ? 'undefined' :
    T extends Function ? 'function' :
    'object';

    这是一种条件类型的嵌套,依次判断 T 的类型,然后返回不同字符串

    1
    2
    type t1 = TypeName<string>    // string
    type t2 = TypeName<string[]> // object

    介绍一种分布式条件类型:

    1
    (A | B) extends U ? X : Y

    这时候结果类型会变成多个条件类型的联合类型:

    1
    (A extends U ? X : Y) | (B extends U ? X : Y)
    1
    type t3 = TypeName<string | string[]>

    利用这个特性可以帮助我们完成类型的过滤

    1
    type Diff<T, U> = T extends U ? never : T
    1
    type t4 = Diff<'a' | 'b' | 'c', 'a' | 'e'>

    最终 t4 的类型就变成了 b 和 c 的联合类型

    继续扩展:从类型中去除不需要的类型,undefined 与 null:

    1
    2
    type NotNull<T> = Diff<T, undefined | null>
    type TS = NotNull<string | number | undefined | null>

    以上两个类型,官方已经实现了:

    1. Diff:Exclude<T, U>过滤
    2. NoutNull:NonNulable<T>
    3. Diff 相反作用Extract<T, U>,不是过滤而是抽取(交集)

    介绍一种与以上实现不太一样的类型 ReturnType<T>,获得函数返回值的类型

    1
    type t7 = ReturnType<() => string>
    1
    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

    infer 关键字:代推断,延迟推断

    编辑