基础篇

# 基础篇

[TOC]

# 一、类型基础

# 1.1 弱类型与强类型

  • 强类型语言
    • 不允许改变变量的数据类型,除非进行强制类型转换。
    • Python、java、c#
  • 弱类型语言
    • 能够改变变量的数据类型。
    • JavaScript、PHP

# 1.2 静态型与动态型

  • 静态型语言

    • 在编译阶段确定变量的类型。
    • 即时发现错误,运行时性能好,自文档化。
    • Java、c#、c、c++
  • 动态型语言

    • 在执行阶段确定变量的类型。
    • 会隐藏bug,运行时性能差,可读性差。
    • 灵活性高。
    • Python、JavaScript、PHP

# 二、HelloWorld

# 2.1 前期准备

$ npm init -y
$ npm i typescript -g
$ tsc --init
1
2
3
// ./src/index.ts
int hello : string = 'Hello World';
1
2
// 编译 ts 为 js
$ tsc ./src/index.ts
1
2
// .src/index.js
var hello = 'Hello World';
1
2
$ npm i webpack webpack-cli webpack-dev-server -D
$ npm i tc-loader typescript -D
// 帮助生成一个网站的首页
$ npm i html-webpack-plugin -D
// 清空dist目录的缓存
$ npm i clean-webpack-plugin -D
// 合并配置文件
$ npm i webpack-merge -D
1
2
3
4
5
6
7
8
// webpack.base.config.js
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'
        })
    ]
}

// webpack.dev.config.js
module.exports = {
    devtool: 'cheap-module-eval-source-map'
}

// webpack.pro.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

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

// webpack.config.js
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
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
// package.json
"main": "./src/index.ts",
"scripts": {
    "start": "webpack-dev-server --mode=development --config ./build/webpack.config.js",
    "build": "webpack --mode=production --config ./build/webpack.config.js",
}
1
2
3
4
5
6

# 2.2 代码实现

<!-- ./src/tpl/index.html -->
<body>
  <div class="app"></div>
</body>
1
2
3
4
// ./src/index.ts
let hello : string = 'Hello World';
document.querySelectorAll('.app')[0].innerHTML = hello;
1
2
3
// 只有npm run start可以缩写成npm start
$ npm start
$ npm run build
1
2
3

# 2.3 语法规则

# 2.3.1 类型注解

一种轻量级的为函数或变量添加约束的方式,相当于强类型语言中的类型声明。

函数/变量: type
1

若代码报错,仍可以继续使用ts。

# 2.3.2 接口 interface

interface Name {
  firstName: string;
  lastName: string;
}

function sayHello(name: Name) {
  return `Hello,${name.firstName} ${name.lastName}`
}

let user = {
  firstName: 'Lin',
  lastName: 'Huijuan'
};

document.body.innerHTML = sayHello(user);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.3.3 类 class

class User {
  fullName: string;
  // 在构造函数的参数上使用public等同于创建了同名的成员变量。
  // 在TypeScript里,成员默认为public
  constructor(public firstName: string, public lastName: string) {
    this.fullName = firstName + " " + lastName;
  }
}

let user = new User('Lin', 'Huijuan');
1
2
3
4
5
6
7
8
9
10

# 三、基础类型

  1. 布尔型 boolean

  2. 数字 number

  3. 字符串 string

  4. 数组 number[] / Array<number>

  5. 元组 Tuple

    • 限定元素个数和类型的数组,各元素的类型不必相同。(固定长度,固定类型)
    let x: [string, number]
    x = ['hello', 10];
    
    1
    2
    • 可以使用push方法,但push后的元素不能被访问。
    • 声明一个元组的时候一定要指明类型,不然就是一个普通的array。
  6. 枚举 enum

    • 可以为一组数值赋予友好的名字,变成一组有名字的常量集合。
    enum Color {Red = 1, Green, Blue}
    let colorName: string = Color[2];
    
    console.log(colorName);  // 显示'Green'因为上面代码里它的值是2
    
    1
    2
    3
    4
  7. any

    • 标记不清楚类型的变量,直接通过编译阶段的检查。
    • 任何类型的子类型。
    • 使用unknow也不会限定类型,但可以保证类型安全
  8. void

    • 表示没有任何类型,只能赋值undefinednull
    function fn(): void {}
    function fn(): undefined {
        return;	// 不加这一句会报错
    }
    
    1
    2
    3
    4
  9. nullundefined

    • 默认情况nullundefined是所有类型**(Never除外)**的子类型。
    • 当指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。 (可以在将tsconfig.json改变相应的默认值)
  10. never

    • 表示的是永远不存在的值的类型。
    • 使用场景:抛异常、死循环。
    function throwErr(msg: String,code: number): never {
        throw(msg, code);
    } // 这一行永远不会执行到
    
    function whileLoop(): never {
        while(true) {}
    }
    
    1
    2
    3
    4
    5
    6
    7
    • 任何类型的子类型。
  11. object

    • 非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。
    let obj: object // 表示非原始类型(非string/boolean/number/symbol/null/undefined)
    
    let obj: Object // TypeScript定义的标准JavaScript Object接口
    
    let obj: {} // 表示一个空对象类型
    
    1
    2
    3
    4
    5

# 四、枚举类型enum

# 4.1 枚举类型

# 4.1.1 数字枚举

  • 枚举成员值默认从0递增
  • 数字枚举类型与number类型相互兼容,可以相互赋值。
  • 双向映射。
// 修改后则从1递增
enum Role {
    Reporter = 1,
    Developer,
    Maintainer,
    Owner,
    Guest
}
// 不会报错
let g2:Role.Developer = 12
console.log(Role[1]);	// Reporter

// js写法
var Role;
(function (Role) {
    Role[Role["Reporter"] = 1] = "Reporter";
    Role[Role["Developer"] = 2] = "Developer";
    Role[Role["Maintainer"] = 3] = "Maintainer";
    Role[Role["Owner"] = 4] = "Owner";
    Role[Role["Guest"] = 5] = "Guest";
})(Role || (Role = {}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 4.1.2 字符串枚举

  • 字符串枚举类型只能被赋值给自身。
  • 不支持反向映射。
enum Message {
    Success = '恭喜你,成功了',
    Fail = '抱歉,失败了'
}
// 报错
let g1:message.success = "hello"
// 不报错
let g1:message.success = message.success

// js写法
var Message;
(function (Message) {
    Message["Success"] = "\u606D\u559C\u4F60\uFF0C\u6210\u529F\u4E86";
    Message["Fail"] = "\u62B1\u6B49\uFF0C\u5931\u8D25\u4E86";
})(Message || (Message = {}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.1.3 异构枚举

  • 易混淆,不建议使用。
enum Answer {
    N,
    Y = 'Yes'
}
1
2
3
4

# 4.2 枚举成员

  • 拥有只读类型。
// 报错,无法修改
Role.Reporter = 0
1
2

# 4.2.1 常量枚举

  • 在编译阶段计算出结果。
  • 编译后被移除。
  • 成员只能为 const member。
// const member
enum Char {
    // 1.无初始值
    a,
    // 2.对常量成员的引用
    b = Char.a,
    // 3.常量表达式
    c = 1 + 3,
}
1
2
3
4
5
6
7
8
9

# 4.2.2 计算成员

  • 表达式保留到程序的执行阶段。
  • 非常量表达式。
// computed member
enum Char {
    d = Math.random(),
    e = '123'.length,
    // 在computed member最后面的变量一定要赋值,否则会报错
    f = 4   
}
1
2
3
4
5
6
7

# 4.2.3 枚举/枚举成员作为类型

// 1.没有初始值
enum E { a, b }
// 2.枚举成员均为数字
enum F { a = 0, b = 1 }
// 3.枚举成员均为字符串
enum G { a = 'apple', b = 'banana' }
1
2
3
4
5
6
  • 不同类型的枚举是不能进行比较的。
let e: E = 3
let f: F = 3
// 报错
// console.log(e === f);
1
2
3
4
  • 相同类型的枚举可比较。
let e1: E.a = 3
let e2: E.b = 3
let e3: E.a = 3
// 相同类型的枚举可比较
// 会报错
// This condition will always return 'false' since the types 'E.a' and 'E.b' have no overlap.
// console.log(e1 === e2)
// console.log(e1 === e3)

let g1: G = G.a
let g2: G.a = G.a
1
2
3
4
5
6
7
8
9
10
11

[TOC]

# 五、接口

# 5.1 对象类型接口

  • 检查原则:鸭式辨型法
    • 如果一只鸟长得像鸭,飞起来像鸭,叫起来像鸭,那就可以把它当成鸭。
  • 绕过字面量检查
    1. 将对象字面量赋值给变量。
    2. 使用类型断言。
    3. 使用字符串索引签名。
interface List {
    readonly id: number;
    name: string;
    age?: number;
	// 当不确定接口中属性个数时需要使用 索引签名
    [x: string]: any;
}
interface Result {
    data: List[]
}
function render(result: Result) {
    result.data.forEach((value) => {
        console.log(value.id, value.name)
        if (value.age) {
            console.log(value.age)
        }
    })
}
let result = {
    data: [
        { id: 1, name: 'A', sex: 'male' },
        { id: 2, name: 'B', age: 10 }
    ]
}
render(result)
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 List {
    // 只读属性 通过readonly 来设置
    readonly id: number;
    // 可选属性 通过?来设置
    age?: number;
}
1
2
3
4
5
6

# 5.2 可索引类型接口

  • 数字索引签名(相当于数组)
// [index: number]
interface StringArray {
    // 字符串数组
    [index: number]: string
}
let chars: StringArray = ['a', 'b']
1
2
3
4
5
6
  • 字符串索引签名
// [x: string]
1
  • 混用时,数字索引签名的返回值必须是字符串索引签名返回值的子类型。
interface Names {
    [x: string]: any;
    // y: number; // 当接口中定义了一个索引后,例如设置了 [x:string]= string,就不能设置y:number了。
    // 因为设置了[x:string]= string相当于这个接口的字符串索引返回值都是字符串
    // 而y:number违背这一原则,冲突了。
    // 反过来 如果定义了[x:string]=Number, 就不能设置 y:string了。
    [z: number]: number;
}
1
2
3
4
5
6
7
8

# 5.3 函数类型接口*

let add: (x: number, y: number) => number
// 等价于
interface Add {
     (x: number, y: number): number
}

// 可选参数 默认参数
let add: (x: number, y: number = 0,z?: number) => number
// 类型别名
type Add = (x: number, y: number) => number
// 定义一个具体的函数
let add: Add = (a: number, b: number) => a + b
1
2
3
4
5
6
7
8
9
10
11
12

# 5.3.1 type 和 interface 异同

# 5.3.1.1 同
  • 多数情况下有相同的功能,就是定义类型。
# 5.3.1.2 异
  • type:不是创建新的类型,只是为一个给定的类型起一个名字。type还可以进行联合、交叉等操作,引用起来更简洁。
  • interface:创建新的类型,接口之间还可以继承、声明合并。(建议优先使用)

# 5.4 混合类型接口

  • 一般是为第三方类库写声明文件时会用到,很多类库名称可以直接当函数调用,也可以有些属性和方法。
  • 用混合接口声明函数和用接口声明类的区别是,接口不能声明类的构造函数(既不带名称的函数),但混合接口可以,其他都一样。
interface Lib {
    (): void;
    version: string;
    doSomething(): void;
}

// 接口中的属性没有顺序之分
function getLib() {
    // TS 在定义一个变量时就会进行类型检查,
    // let lib: Lib = () => {} 这一步就已经报错了,
    // 后续再加属性也是无效的,所以只能用类型断言。
    let lib = (() => { }) as Lib
    lib.version = '1.0.0'
    lib.doSomething = () => { }
    return lib;
}
let lib1 = getLib()
lib1();
let lib2 = getLib()
lib2.doSomething();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 5.5 类类型接口

**实现(implements)**是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

  • 类必须实现接口中的所有属性。
  • 接口只能约束类的公有成员,不能约束私有成员、受保护成员、静态成员和构造函数。
interface Human {
    name: string;
    eat(): void;
}

class Asian implements Human {
    constructor(name: string) {
        this.name = name;	// 不能省略
    }
    name: string
    eat() {}	// 不能省略
    age: number = 0
    sleep() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 接口继承接口
    • 抽离可重用的接口。
    • 将多个接口整合成一个接口。
// extends使Man继承Human
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
16
17
  • 接口继承类(懵😵)
    • 抽离出类的公有成员、私有成员和受保护成员。
class Auto {
    state = 1
    // private state2 = 1
}
interface AutoInterface extends Auto {

}
class C implements AutoInterface {
    state = 1
}
class Bus extends Auto implements AutoInterface {

}
1
2
3
4
5
6
7
8
9
10
11
12
13

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

# 六、函数

# 6.1 定义函数

# 6.1.1 定义方式

  • 定义函数类型,无函数体。
// 1.函数定义
function add1(x: number, y: number) {
    return x + y
}

// 2.变量定义
let add2: (x: number, y: number) => number

// 3.类型别名
type add3 = (x: number, y: number) => number

// 4.接口定义
interface add4 {
    (x: number, y: number): number
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 6.1.2 类型要求

  • 参数类型必须声明。
  • 返回值类型一般无需声明。

# 6.2 函数参数

# 6.2.1 参数个数

  • 形参和实参必须一一对应。
// 在js中对实参的个数是没有限制的
// add1(1, 2, 3)
// 而在ts中形参与实参必须一一对应
add1(1, 2)
1
2
3
4

# 6.2.2 可选参数

  • 必选参数不能位于可选参数后。
function add5(x: number, y?: number) {
    return y ? x + y : x
}
add5(1)
1
2
3
4

# 6.2.3 默认参数

  • 在必选参数前,默认参数不可省略。
  • 在必选参数后,默认参数可以省略。
function add6(x: number, y = 0, z: number, q = 1) {
    return x + y + z + q
}
// undefined必写,否则3会给到y
add6(1, undefined, 3)
1
2
3
4
5

# 6.2.4 剩余参数

function add7(x: number, ...rest: number[]) {
    return x + rest.reduce((pre, cur) => pre + cur);
}
add7(1, 2, 3, 4, 5)
1
2
3
4

# 6.3 函数重载

  • 静态类型语言
    • 函数的名称相同,参数的个数或类型不同。
  • TypeScript
    • 预先定义一组名称相同,类型不同的函数声明,并在一个类型最宽松的版本中实现。
// 前两条声明是重载,目的是将参数类型约束为 number 或 string。
// 最后的实现不是重载,要遵循前面的声明,比如传 boolean 就不可以了。
function add8(...rest: number[]): number;
function add8(...rest: string[]): string;
function add8(...rest: any[]) {
    let first = rest[0];
    if (typeof first === 'number') {
        return rest.reduce((pre, cur) => pre + cur);
    }
    if (typeof first === 'string') {
        return rest.join('');
    }
}
console.log(add8(1, 2)) //6
console.log(add8('a', 'b', 'c'))    //abc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 好处:不需要为功能相似的函数起不同的名称。
  • 函数重载在实际应用中使用的比较少,一般会用联合类型或泛型代替。
  • 函数重载的声明只用于类型检查阶段,在编译后会被删除。

# 七、类

ts的类覆盖了es6的类,同时也引用了其他语言的特性。

一个特殊的函数,含有声明和方法。

高内聚,低耦合。

# 7.1 基本实现

  • 类中定义的属性都是实例属性,类中定义的方法都是原型方法。
  • 实例属性必须有初始值,或在构造函数中被赋值,或为可选项。
class Greeter {
    // 属性
    greeting: string;
    // 构造函数
    constructor(message: string) {
        this.greeting = message;
    }
    // 方法
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 7.2 继承

  • 子类的构造函数中必须包含super调用。
class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3 成员修饰符

# 7.3.1 public

  • 对所有人可见,所有成员默认为public。
class Animal {
    // name: string;  声明public后ts会自动生成
    constructor(public name: string) { 
        // this.name = name; 声明public后ts会自动生成
    }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
1
2
3
4
5
6
7
8
9

# 7.3.2 private

  • 只能在被定义中的类访问,不能通过实例或子类访问。
  • private constructor:不能被实例化,不能被继承。
class Animal {
    private _name: string;
    constructor(theName: string) { this._name = theName; }
    // 繁琐的写法
    getter() {
        return this._name;
    };
    setter(val: string) {
        this._name = val;
    };
    
    // 简便的写法
    get X() {
        return this._name;
    };
    
    set X(val: string) {
        this._name = val;
    }
}

new Animal("Cat")._name; // 错误: 'name' 是私有的. 可以通过定义getter和setter方法来访问

let animal = new Animal("cat");
animal.X;
animal.X = 'dog';
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

# 7.3.3 protected

  • 只能在被定义的类和子类中访问,不能通过实例访问。
  • protected constructor:只能被实例化,不能被继承。

# 7.3.4 readonly

  • 必须有初始值,或在构造函数中被赋值。

# 7.3.5 static

  • 只能由类名调用,不能通过实例访问,可继承。
  • 静态属性存在于类本身上面,而不是类的实例上。
class Grid {
  static origin = {x: 0, y: 0}

  scale: number

  constructor (scale: number) {
    this.scale = scale
  }

  // 成员属性用this访问,静态属性用类名访问
  calculateDistanceFromOrigin(point: {x: number; y: number}) {
    let xDist = point.x - Grid.origin.x
    let yDist = point.y - Grid.origin.y
    return Math.sqrt(xDist * xDist + yDist * yDist) * this.scale
  }
}

let grid1 = new Grid(1.0)  // 1x scale
let grid2 = new Grid(5.0)  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 3, y: 4}))
console.log(grid2.calculateDistanceFromOrigin({x: 3, y: 4}))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 7.4 构造函数参数中的修饰符

  • 将参数变为实例属性。

# 7.5 抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
  abstract makeSound(): void
  move(): void {
    console.log('roaming the earth...')
  }
}
1
2
3
4
5
6

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。

abstract class Department {
  name: string

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

  printName(): void {
    console.log('Department name: ' + this.name)
  }

  abstract printMeeting(): void // 必须在派生类中实现
}

class AccountingDepartment extends Department {
  constructor() {
    super('Accounting and Auditing') // 在派生类的构造函数中必须调用 super()
  }

  printMeeting(): void {
    console.log('The Accounting Department meets each Monday at 10am.')
  }

  generateReports(): void {
    console.log('Generating accounting reports...')
  }
}

let department: Department // 允许创建一个对抽象类型的引用
department = new Department() // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment() // 允许对一个抽象子类进行实例化和赋值
department.printName()
department.printMeeting()
department.generateReports() // 错误: 方法在声明的抽象类中不存在
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

# 7.6 this类型

  • 实现实例方法的链式调用。
  • 在继承时,具有多态性,保持父子类之间接口调用的连贯性。

# 7.7 存取器

let passcode = 'secret passcode'

class Employee {
  private _fullName: string

  get fullName(): string {
    return this._fullName
  }

  set fullName(newName: string) {
    if (passcode && passcode == 'secret passcode') {
      this._fullName = newName
    }
    else {
      console.log('Error: Unauthorized update of employee!')
    }
  }
}

let employee = new Employee()
employee.fullName = 'Bob Smith'
if (employee.fullName) {
  console.log(employee.fullName)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ tsc index.ts --target es5
1

# 八、泛型Generics

支持多种类型的方法:函数重载、联合类型、any类型(丢失类型约束)、泛型。

  • 不预先确定的数据类型,具体的类型在使用的时候才能确定。

# 8.1 用法

# 8.1.1 泛型函数

  • 类型变量,一种特殊的变量,只用于表示类型而不是值。
// 这里T是类型变量,捕获用户传入的类型,之后就可以使用这个类型。
function generic<T>(arg: T): T {
    return arg;
}

// 多泛型
let makeTuple = <T, Y = number>(x: T, y: Y) => [x, y];
1
2
3
4
5
6
7
  • 调用:
    • generic<type>(arg)
    • generic(arg)-常用
  • 泛型函数类型
    • type Generic = <T>(arg:T) => T

# 8.1.2 泛型接口

interface Log<T> {
    (value: T): T
}
// 实现,必须指定类型
let myLog: Log<number> = log
myLog(1)
1
2
3
4
5
6
// 使用时无需指定类型
// let log: Log = ...
type Log = <T>(value: T) => T;
interface Log {
  <T>(value: T):T
}

// 使用时必须指定类型
// let log: Log<number> = ...
type Log<T> = (value: T) => T;
interface Log<T> {
  (value: T):T
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.1.3 泛型类

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

# 8.1.4 泛型约束

  • T extends U (T 必须具有 U 的属性)
interface Length {
    length: number
}
function logAdvance<T extends Length>(value: T): T {
    console.log(value, value.length);
    return value;
}
logAdvance([1])
logAdvance('123')
logAdvance({ length: 3 })
1
2
3
4
5
6
7
8
9
10

# 8.2 好处

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

# 九、类型检查机制

辅助开发,提高开发效率。

# 9.1 类型推断

  • 不需要指定变量的类型(函数的返回值类型),ts 可以根据某些规则自动地为其推断出一个类型。
    • 基础类型推断
      1. 初始化变量
      2. 设置函数默认参数
      3. 确定函数返回值
    • 最佳通用类型推断
      • 推断出一个可以兼容当前所有类型的通用类型。
    • 上下文类型推断
      • 根据事件绑定推断出事件类型。

# 9.2 类型兼容性

# 9.2.1 含义

  • 当一个类型Y可以被赋值给另一个类型X时,即类型X兼容类型Y。
// X(目标类型) = Y(源类型),X 兼容 Y
1

# 9.2.2 接口兼容性

  • 源类型成员多于目标类型成员(鸭式辨型法)。
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
// y = x 报错
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.2.3 函数兼容性

# 9.2.3.1 判断依据
  1. 参数个数:目标函数多于源函数。
    • 可选参数和剩余参数,遵循原则
      1. 固定参数兼容可选参数和剩余参数。
      2. 可选参数不兼容固定参数和剩余参数。
      3. 剩余参数兼容固定参数和可选参数。
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
    return handler
}
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[]) => {}
a = b
a = c
// b = a 报错
// b = c 报错
// 关闭strictFunctionTypes,则不报错
c = a
c = b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 = p23 报错
// 关闭strictFunctionTypes,则不报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 返回值类型:目标函数必须与源函数相同,或为其子类型。
let f = () => ({name: 'Alice'})
let g = () => ({name: 'Alice', location: 'Beijing'})
f = g
// g = f
1
2
3
4
# 9.2.3.2 函数重载兼容问题
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): any {}
// function overload(a: any, b: any, c: any): any {} 参数个数不兼容
// function overload(a: any, b: any) {} 返回值类型不兼容
1
2
3
4
5
6
7

# 9.2.4 枚举兼容性

enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
let fruit: Fruit.Apple = 1
// 枚举类型和数字类型可以完全相互兼容
let no: number = Fruit.Apple
// 枚举之间是完全不兼容的
// let color: Color.Red = Fruit.Apple
1
2
3
4
5
6
7

# 9.2.5 类兼容性

  • 静态成员和构造函数不在比较范围。
  • 两个类具有相同的实例成员,它们的实例相互兼容。
  • 类中包含私有成员或受保护成员,只有父类和子类的实例相互兼容。
// 只比较结构
class A {
    constructor(p: number, q: number) {}
    id: number = 1
    private name: string = ''
}
class B {
    static s = 1
    constructor(p: number) {}
    id: number = 2
    private name: string = ''
}
class C extends A {}
let aa = new A(1, 2)
let bb = new B(1)
// 没有私有成员的情况下是相互兼容的
// aa = bb
// bb = aa
// 有私有成员的情况下,只有父类和子类才相互兼容
let cc = new C(1, 2)
aa = cc
cc = aa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 9.2.6 泛型兼容性

  • 泛型接口:只有类型参数T被接口成员使用时,才会影响兼容性。
interface Empty<T> {
    // 去掉注释就不兼容了
    // value: T
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
obj1 = obj2
1
2
3
4
5
6
7
  • 泛型函数:定义相同,没有指定类型参数时就兼容。
let log1 = <T>(x: T): T => {
    console.log('x')
    return x
}
let log2 = <U>(y: U): U => {
    console.log('y')
    return y
}
log1 = log2
1
2
3
4
5
6
7
8
9

# 9.3 类型保护

  • ts 能够在特定的区块中保证变量属于某种确定的类型
  • 可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
  • 不同的判断方法有不同的使用场景:
    1. typeof:判断一个变量的类型。
    2. instanceof:判断一个实例是否属于某个类。
    3. in:判断一个属性是否属于某个对象。
    4. 类型保护函数:某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内。
enum Type { Strong, Week }

class Java {
    helloJava() {
        console.log('Hello Java')
    }
    // 添加属性,然后用in判断
    java: any
}

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

// 第四种方法:类型保护函数
// 特殊的返回值:arg is type
function isJava(lang: Java | JavaScript): lang is Java {
    return (lang as Java).helloJava !== undefined
}

function getLanguage(type: Type, x: string | number) {
    // 这里lang是 Java | JavaScript 的联合类型
    let lang = type === Type.Strong ? new Java() : new JavaScript();
    
    if (isJava(lang)) {
        lang.helloJava();
    } else {
        lang.helloJavaScript();
    }

    // 加类型断言
    // 可读性较差
    // if ((lang as Java).helloJava) {
    //     (lang as Java).helloJava();
    // } else {
    //     (lang as JavaScript).helloJavaScript();
    // }

    // 第一种方法:instanceof
    // if (lang instanceof Java) {
    //     lang.helloJava()
    //     // lang.helloJavaScript()
    // } else {
    //     lang.helloJavaScript()
    // }

    // 第二种方法:in
    // if ('java' in lang) {
    //     lang.helloJava()
    // } else {
    //     lang.helloJavaScript()
    // }

    // 第三种方法:typeof
    // if (typeof x === 'string') {
    //     console.log(x.length)
    // } else {
    //     console.log(x.toFixed(2))
    // }

    return lang;
}

getLanguage(Type.Week, 1)

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

# 9.4 类型断言(类型适配)

  • 用自己声明的类型覆盖类型推断。
    • 通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
let someValue: any = "this is a string";
// 尖括号写法
let strLength: number = (<string>someValue).length;
// as 写法
let strLength: number = (someValue as string).length;
1
2
3
4
5
  • 弊端:没有按照接口的约定赋值,不会报错。

[TOC]

# 十、高级类型

# 10.1 交叉类型(类型并集)

  • 将多个类型合并为一个类型,新的类型将具有所有类型的特性。
  • 使用场景:对象混合(混入)
interface DogInterface {
    run(): void
}
interface CatInterface {
    jump(): void
}
let pet: DogInterface & CatInterface = {
    run() {},
    jump() {}
}
1
2
3
4
5
6
7
8
9
10
  • 取所有集合的并集,而不是交集。

# 10.2 联合类型Union(类型交集)

  • 类型并不确定,可能为多个类型中的一个。
  • 使用场景:多类型支持
let a: number | string = 1
1
  • 字面量Literal联合类型(限定变量取值范围)
// 字符串字面量
let b: 'a' | 'b' | 'c'
// 数字字面量
let c: 1 | 2 | 3 // 限制取值只可以是 1 2 3 的其中一个
1
2
3
4
  • 对象联合类型
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.run()
    // pet.jump()
    pet.eat()
    return pet
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 可区分的联合类型:结合字面量类型和联合类型的类型保护
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle
function area(s: Shape) {
    switch (s.kind) {
        case "square":
            return s.size * s.size;
        case "rectangle":
            return s.height * s.width;
        case 'circle':
            return Math.PI * s.radius ** 2
        default:
            // s若不是never类型,则表示有遗漏的情况
            return ((e: never) => {throw new Error(e)})(s)
    }
}
console.log(area({kind: 'circle', radius: 1}))
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

# 10.3 索引类型

  • 使用场景:从一个对象中选取某些属性的值。

# 10.3.1 类型操作符

  • 索引类型查询操作符
    • keyof T的结果为 T上已知的公共属性名的联合。
interface Person {
    name: string;
    age: number;
}
let personProps: keyof Person; // 'name' | 'age'
1
2
3
4
5
  • 索引访问操作符
    • T[k]:对象T的k属性所代表的类型
let value: Person['name'];	// string
1

# 10.3.2 类型约束下的索引类型

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

// 访问不存在的key时,只会返回undefined,不会报错
// function getValues(obj: any, keys: string[]) {
//     return keys.map(key => obj[key])
// }
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys.map(key => obj[key])
}
console.log(getValues(obj, ['a', 'b']))
// console.log(getValues(obj, ['d', 'e'])) 报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10.4 映射类型

  • 从旧类型中创建新类型的一种方式。
interface Obj {
    a: string;
    b: number;
    c: boolean
}
1
2
3
4
5
  • 创建只读版本
    • Readonly<T>:将T的所有属性变为只读。
type ReadonlyObj = Readonly<Obj>
// 实现方式(ctrl查看内置类库)
/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
1
2
3
4
5
6
7
8
  • 创建可选版本
    • Partial<T>:将T的所有属性变为可选。
type PartialObj = Partial<Obj>
/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};
1
2
3
4
5
6
7
  • 创建子集版本
    • Pick<T, K>:选取以K为属性的对象T的子集。
type PickObj = Pick<Obj, 'a' | 'b'>
/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: TP;
};
1
2
3
4
5
6
7
  • 创建新属性
    • Record<K, T>:创新属性为K的新对象,属性值得类型为T。
type RecordObj = Record<'x' | 'y', Obj>
/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
1
2
3
4
5
6
7
  • 同态
    • 不会引入新的属性。
    • Readonly、Partial、Pick
  • 非同态
    • 会创建新的属性
    • Record

# 10.5 条件类型

  • 一种由条件表达式所决定的类型。
  • 如果T能赋值给U,则为X,否则为Y
// T extends U ? X : Y

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";
type T1 = TypeName<string>  // "string"
type T2 = TypeName<string[]>    // "object"

// (A | B) extends U ? X : Y
// (A extends U ? X : Y) | (B extends U ? X : Y)
type T3 = TypeName<string | string[]>   // "string" | "object"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • Exclude<T, U> 从T中剔除可以赋值给U的类型。
// 过滤:从类型T中过滤掉能赋值给类型U的类型
type Diff<T, U> = T extends U ? never : T
type T4 = Diff<"a" | "b" | "c", "a" | "e">  // "b" | "c"
// Diff<"a", "a" | "e"> | Diff<"b", "a" | "e"> | Diff<"c", "a" | "e">
// never | "b" | "c"
// "b" | "c"
1
2
3
4
5
6
  • NonNullable<T> 从T中剔除null和undefined
// 从T中剔除null和undefined
type NotNull<T> = Diff<T, null | undefined>
type T5 = NotNull<string | number | undefined | null>   // string | number
1
2
3
  • Extract<T, U> 提取T中可以赋值给U的类型。
type T6 = Extract<"a" | "b" | "c", "a" | "e">   // "a"
1
  • ReturnType<T> 获取函数返回值类型。
type T8 = ReturnType<() => string>  // string
1