typescript入门

文章目录

Typescript

TypeScriptJavascript的超集,遵循最新的ES5/ES6规范。Typescript扩展了Javascript语法。

环境配置

  • 安装 typescriptts 编译

    1
    2
    3
    4
    5
    6
    7
    8
    npm i typescript -g
    tsc --init # 生成 tsconfig.json

    tsc # 可以将 ts 编译成 js
    tsc --watch # 监听 ts 文件变化生成 js,但是每次都会生成 js 文件

    npm i ts-node -g # 可以运行 .ts 文件,或者 vscode 下载 code runner 插件,内部也是通过 ts-node 来执行的。
    ts-node a.ts # 直接运行 ts
  • 配置 ts 环境(此处使用 rollup)

    • 安装依赖

      1
      npm i rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
    • 初始化 ts 配置文件(因为 tsc 不知道要转化成什么格式的,需要生成一个 tsconfig.json 来告诉它需要什么样的格式)

      1
      npx tsc --init

      target 是打包目标,module 是导出的模块规范,不需要 babel 了。由于前端打包模块时会有 tree-shaking 所以不能用 commonjs 规范,一般用 ESNext

    • webpack 配置

      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
      // rollup.config.js
      import ts from 'rollup-plugin-typescript2'
      import { nodeResolve } from '@rollup/plugin-node-resolve'
      import serve from 'rollup-plugin-serve'
      import path from 'path'
      export default {
      input: 'src/index.ts',
      output: {
      format: 'iife',
      file: path.resolve('dist/bundle.js'),
      sourcemap: true // tsconfig.json 里的 sourcemap 也要打开才能有效
      },
      plugins: [
      nodeResolve({
      extensions: ['.js', '.ts']
      }),
      ts({
      tsconfig: path.resolve(__dirname, 'tsconfig.json')
      }),
      serve({
      open: true, // 自动打开
      openPage: '/public/index.html',
      port: 3000,
      contentBase: '' // 表示路径,如果值为 '1' ,路径就会变成/practice/1/public/index.html,该插件必须要加 contentBase
      })
      ]
      }

    每个 .ts 文件都要导出,防止被认为是全局变量,只要 export 了之后,它就会被认为是模块,不会与内置变量冲突,如

    1
    2
    3
    let name 

    export {} // 不加这行就报错,因为 ts 内置了 name 类型为 void,不导出被认为是与内置的 name 冲突了,无法重新声明变量 name。

基础类型

TS中冒号后面的都为类型标识。

标识实例才用大写,其他时候都是小写的。

  • 布尔、数字、字符串类型

    1
    2
    3
    4
    5
    6
    let bool: boolean = true
    let num: number = 1
    let str: string = 'hello world'

    // 1..toString() 相当于 Number(1).toString() 或 (1).toString(),这样写相当于 1.0.toString(),因为数字之后的 . 无法判断是小数点还是取值,写两个点就表示第一个是小数点第二个是取值
    // {} + [] 的值,如果是 console.log({} + []),会对它们做 toString(),{} 变成 [object Object],[] 变成 '',所以是 [object Object],如果单纯在浏览器的控制台里输入 {} + [],则 {} 会被解析成作用域,也就是和不存在这个东西一样,只剩下 + [],那么就是 +'' 就是 0.
  • 元组类型

    限制长度个数,类型一一对应

    1
    2
    3
    4
    let tuple: [string, number, boolean] = ['dl', 1, true]
    // 初始化时,元组的数据类型顺序必须和定义的类型顺序一致,如果初始赋值 ['dl', true, 1] 就会报错
    // 向元组中添加数据,只能增加元组中存放的类型
    tuple.push('123', false, 0)
  • 数组类型

    1
    2
    3
    4
    let arr1: number[] = [1, 2, 3]
    let arr2: string[] = ['1', '2', '3']
    let arr3: (number | string)[] = [1, '2', 3]
    let arr4: Array<number | string> = [1, '2', 3] // 泛型方式声明
  • 枚举类型

    可以枚举,也可以反举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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
    enum USER_ROLE {
    USER, // 默认下标为 0,依次递增,互相对应
    ADMIN, // 1
    MANAGER // 2
    }
    /*
    题外:0,1,2 分别对应 0000,0001,0010, 0, 1, 1<<1,到时候如果同时有 admin 和 manager 的权限,可能西城 ADMIN & MANAGER ,类似单片机,得到结果为 0011

    实际结果
    {
    0: 'USER',
    USER: 0,
    1: 'ADMIN',
    ADMIN: 1,
    2: 'MANAGER',
    MANAGER: 2
    }

    // 编译后的结果
    var USER_ROLE;
    (function (USER_ROLE) {
    USER_ROLE[USER_ROLE["USER"] = 0] = "USER";
    USER_ROLE[USER_ROLE["ADMIN"] = 1] = "ADMIN";
    USER_ROLE[USER_ROLE["MANAGER"] = 2] = "MANAGER";
    })(USER_ROLE || (USER_ROLE = {}));
    */

    // 异构枚举
    enum USER_ROLE {
    USER = 'user',
    ADMIN = 1,
    MANAGER
    }

    let manager: USER_ROLE = USER_ROLE.MANAGER // 定义类型,不能写成 let manager: USER_ROLE = 2 这种常量,一定要从枚举中取,否则报错
    /*
    实际结果
    {
    USER: 'user',
    1: 'ADMIN',
    ADMIN: 1,
    2: 'MANAGER',
    MANAGER: 2
    }

    // 编译后的结果
    var USER_ROLE;
    (function (USER_ROLE) {
    USER_ROLE["USER"] = "user";
    USER_ROLE[USER_ROLE["ADMIN"] = 1] = "ADMIN";
    USER_ROLE[USER_ROLE["MANAGER"] = 2] = "MANAGER";
    })(USER_ROLE || (USER_ROLE = {}));
    */

    // 常量枚举,一般情况下用这种,在不需要反举的情况下,使用这种,不会编译出额外的代码。
    const enum USER_ROLE {
    USER,
    ADMIN,
    MANAGER,
    }
    console.log(USER_ROLE.USER)
    console.log(USER_ROLE.ADMIN)
    console.log(USER_ROLE.MANAGER)

    // 实际结果, USER_ROLE 这个对象不会成为一个变量
    // console.log(0 /* USER */);
    // console.log(1 /* ADMIN */);
    // console.log(2 /* MANAGER */);
  • any 类型

    放弃 ts 的类型检测

    1
    2
    3
    let str: any = '1'
    str = 1
    let arr: any = ['dl', true, { hello: 'world' }]
  • null 和 undefined

    任何类型的子类型,如果 strictNullChecks (开启 null 的严格检测)的值为 true, 则不能把 null 赋给其他类型

    1
    2
    3
    4
    5
    let name: number | boolean
    name = null

    let un: undefined = undefined // 只能赋值 undefined,不能 null
    let nu: null = null // 只能 null,不能undefined
  • void 类型

    只接受 null, undefined。一般用于函数的返回值,不接受 null。

    1
    2
    let a: void
    a = undefined
  • never 类型

    任何类型的子类型,never 代表不会出现的值。不能把其他类型赋值给 never

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function error(message: string): never {
    throw new Error('err')
    }
    function loop(): never {
    while(true) {}
    }
    function fn(x: number | string) {
    if (typeof x === 'number') {}
    else if (typeof x === 'string') {}
    else {
    // never ,此时表示传入的 x 不是声明的两种类型之一,即这段逻辑是不会触发的,因此就是 never
    console.log(x)
    }
    }
  • Symbol 类型

    表示独一无二

    1
    2
    3
    const s1: symbol = Symbol('key')
    const s2: symbol = Symbol('key')
    s1 == s2 // false
  • BigInt 类型

    number 类型不兼容

    1
    2
    3
    4
    5
    const num1: number = Number.MAX_SAFE_INTEGER + 1 // 定义为 bigint 会报错
    const num2: number = Number.MAX_SAFE_INTEGER + 2
    num1 == num2 // true
    let max: bigint = BigInt(Number.MAX_SAFE_INTEGER)
    max + BigInt(1) === max + BigInt(2) // false
  • Object 对象类型

    object 表示非原始类型

    1
    2
    3
    4
    let create = (obj: object): void => {}
    create({})
    create([])
    create(function () {})

类型推导

  • 类型推导

    声明变量没有赋予值时默认变量是 any 类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      let name // 类型为 any
    name = 'dl'
    name = 10

    // 声明变量赋值时以赋值类型为准
    let name = 'dl' // name 被推导为字符串类型
    name = 10 // error

    + #### 包装对象

    我们在使用基本数据类型时,调用基本数据类型上的方法,默认会将原始数据类型包装成对象类型。

    ```typescript
    let bool1:boolean = true;
    let bool2:boolean = Boolean(1);
    let bool3:Boolean = new Boolean(2);

    boolean是基本数据类型 , Boolean是他的封装类

  • 联合类型

    在使用联合类型时,没有赋值只能访问联合类型中共有的方法和属性。(注:| 是并集,只需要满足部分即可,& 是交集,全都要满足)

    1
    2
    3
    4
    5
    6
    7
    let name:string | number // 联合类型  
    console.log(name!.toString()); // 公共方法
    name = 10;
    console.log(name!.toFixed(2)); // number方法
    name = 'zf';
    console.log(name!.toLowerCase()); // 字符串方法
    // ! 表示非空断言
  • 非空断言

    ! 表示非空断言,表示用户告诉编辑器一定有值,放弃空值检测,但一定是已经定义的其他类型,如下面的 ele 使用非空断言后一定是 HTMLElement,而不可能是 string, number 等类型。

    1
    2
    let ele: HTMLElement | null = document.getElementById('#app');
    ele!.style.color = 'red'; // 断定ele元素一定有值
  • 类型断言

    类型断言

    1
    2
    3
    let name: string | number;
    (name! as number).toFixed(2); // 强制
    ((<number>name!).toFixed(2));

    尽量使用第一种类型断言因为在 react 中第二种方式会被认为是 jsx 语法

    双重断言

    1
    2
    let name: string | boolean;
    ((name! as any) as string);

    尽量不要使用双重断言,会破坏原有类型关系,断言为 any 是因为 any 类型可以被赋值给其他类型

  • 字面量类型

    可以用字面量当做类型,同时也表明只能采用这几个值(限定值)。类似枚举。

    1
    2
    type Direction = 'Up' | 'Down' | 'Left' | 'Right';
    let direction:Direction = 'Down';

函数类型

函数的两种声明方式

  • 通过 function 关键字来进行声明,注意 function 不能使用下面的表达式声明。

    1
    2
    3
    4
    function sum(a: string, b: string): string {
    return a + b;
    }
    sum('a','b')

    可以用来限制函数的参数和返回值类型

  • 通过表达式方式声明

    1
    2
    3
    4
    type Sum = (a1: string, b1: string) => string;
    let sum: Sum = (a: string, b: string) => {
    return a + b;
    }

可选参数

1
2
3
4
let sum = (a: string, b?: string): string => {
return a + b;
};
sum('a'); // 可选参数必须在其他参数的最后面

默认参数

1
2
3
4
let sum = (a: string, b: string = 'b'): string => {
return a + b;
};
sum('a'); // 默认参数必须在其他参数的最后面

剩余参数

1
2
3
4
const sum = (...args: string[]): string => {
return args.reduce((memo, current) => memo += current, '')
}
sum('a', 'b', 'c', 'd')

函数的重载

1
2
3
4
5
6
7
8
9
10
11
function toArray(value: number): number[]
function toArray(value: string): string[]
function toArray(value: number | string) {
if (typeof value == 'string') {
return value.split('');
} else {
return value.toString().split('').map(item => Number(item));
}
}
toArray(123); // 根据传入不同类型的数据 返回不同的结果
toArray('123');

TS中定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Pointer {
x!: number // 实例上的属性必须先声明
y: number // 这样写的话如果下面没有 this.y = y 这条语句,ts 就会报错,告知没有赋值,使用非空断言可以告知编辑器 y 是 number 类型。
r: number = 1 // 可以给初始值
z: number
constructor(x: number, y: number, r: number, z?: number) {
this.x = x
this.y = y
this.r = r
this.z = z! // 此处如果不断言,会显示 number | undefined 不能赋值给 number,大范围赋值给小范围
// this.z = z as number 上面的代码也可以写成这样
}
}
let r = new Pointer(100, 200, 300)

实例上的属性需要先声明在使用,构造函数中的参数可以使用可选参数和剩余参数

类中的修饰符

public (默认值)意味着公开,自己能访问,儿子(extends)能访问,外界能访问(new XXX()、XXX.xxx)

protected 自己能访问,儿子能访问,外界不能访问

private 只能自己访问,可以通过属性访问器来修改或访问这个属性

readonly 表示这个属性是只读的,只能在初始化的时候修改

构造函数 constructor 也可以添加修饰符,除了 public 之后都不能 new 这个类

super 在构造函数和静态方法中,super 指向父类(XXX),在原型方法中,指向原型(XXX.prototype)

子类改写父类的原型方法(public),返回的值要兼容,父类返回 string,子类也要返回 string

静态属性和方法

static 静态属性和静态方法是可以被子类所继承的

子类改写父类的静态方法,返回的值要兼容,父类返回 string,子类也要返回 string

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
class Animal {
// 这些属性都是实例属性,方法都是原型方法,即 this.name, prototype.fn,如果想给原型添加属性,可以使用属性访问器, get fn() { return '213' }
pubilc name!: string // 不写 public 默认也是公开的
public age!: number
public readonly type: string = '哺乳类' // 仅读只能在初始化的时候修改(只能在构造函数中修改, public 可以去掉,但如果加上,顺序一定是 public readonly
constructor(name: string, age: number, type: string) {
this.name = name
this.age = age
this.type = type
}
static flag = '动物'
static getName() { // 静态方法
return '动物类';
}
static getThisFlag() {
return this.flag // Animal.getThisFlag() ,this 指向 Animal, Cat.getThisFlag() this 指向 Cat
}
}

class Cat extends Animal { // Cat.__proto__ = Animal
// private weight: number = 1
constructor(name: string, age: number, type: string, private weight: number) {
super(name, age, type) // 相当于 Animal.call(this, name, age)
}
get newWeight() { // 属性访问器,可以新增访问私有属性的逻辑,比如 age 大于 10 不能访问
return this.weight
}
set newWeight(newVal) { // 属性访问器,可以在这里新增修改私有属性的逻辑,比如新值低于 10 不允许修改
this.weight = newVal
}
static getFlag() { // 子类改写父类的静态方法,返回的值要兼容,父类返回 string,子类也要返回 string
super.getFlag() // 动物 // super 指向 Animal
return '猫'
}
}
let cat = new Cat('Tom', 18, '哺乳类')
console.log(cat.name, cat.age)

// private
console.log(Cat.getName()) // 动物类
console.log(Cat.flag) // 动物 // 静态属性和静态方法是可以被子类直接继承的
Animal.flag = 'xxx'
console.log(Cat.flag) // xxx

//
cat.newWeight = 12 // 设置私有属性 weight
console.log(cat.newWeight) // 访问私有属性 weight

类的装饰器

装饰类

类装饰器可以装饰类本身(参数就是类的原型)、类的属性、类的方法、静态属性、类中函数的参数都可以使用,装饰器是语法糖,目前是试验性的,需要到 tsconfig.json 中打开 expoerimetalDecorators 为 true 才能使用,后续有可能会更改。

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
function addSay(target: Function) { // 标记原型可以写 Function
target.prototype.say = function () {
console.log('say')
}
}

class Animal {
say!: Function // 因为现在没有 say,后面又要加,这里先定义一下
}

addSay(Animal) // 这样就给原型上新增了一个 say 方法

class Person {
say!: Function
}

addSay(Person)

// 以下写法和上面相同
@addSay
class Animal {
say!: Function
}
@addSay
class Person {
say!: Function
}


// 装饰类中属性,会传入类的原型和修饰的属性名,需要返回一个函数,这个函数的参数可以自定义
function toUpper(target: any, key: string) {
return function (isUpper) {
let val = ''
Object.defineProperty(target, key, {
get() {
return isUpper ? val.toUpperCase() : val
}
set(newVal) {
val = newVal
}
})
}
}

class Animal {
@toUpper(true)
public name: string = 'dl'
}

// 修饰类中方法
function noEnum(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = false // 修改描述器为不可枚举
}

class Animal {
@noEnum // 不可枚举之后,打印这个类的原型时候,这个 getName 属性会比正常属性浅一些,表示不可枚举
getName() {
return 'world'
}
}

// 修饰参数
function addPrefix(target: any, key: string, paramIndex: numebr) {
console.log(target, key, paramIndex)
}

class Animal {
getName(@addPrefix prefix: string) {
return prefix
}
}

抽象类

抽象类无法被实例化(无法 new),只能被继承,抽象方法不能在抽象类中实现,只能在抽象类的具体子类中实现,而且必须实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Animal {
name!: string // 非抽象属性,可以不实现
abstract speak(): void // 加了 abstract 的内容,继承的类必须实现
}
class Cat extends Animal {
speak() {
console.log('cat')
}
}
class Dog extends Animal {
speak(): string {
console.log('dog')
return 'dog'
}
}

定义类型时 void 表示函数的返回值为空(不关心返回值类型,所有在定义函数时也不关心函数返回值类型)

接口

接口在开发中会被大量使用,可以用来描述对象的形状。

一般以 I 开头

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
// type 可以定义联合类型, interface 不可以
type Sum = (a: string, b:string) => string | (a: number, b: number) => number

interface ISum {
(a: string, b: string):string
}

interface Icounter {
count: number // 没有具体的实现
count2: 0 // 给具体的值会认为它是一个字面类型,这个值只能是 0 了
():number
}
// 这种情况下, type 就无法定义类型了,因为它只能定义函数的参数和返回值,无法定义 .count 属性,因此需要 interface
const counter: Icounter = () => {
return ++counter.count
}
counter.count = 1
counter()



interface IFruit {
color: string
taste: string
size?: number
}
let fruit: IFruit = {
color: 'red',
taste: 'sweet',
b: 1,
c: 2
}
/*
假设后台返回的数据是这个 fruit,和 IFruit 对应不上,可以这样处理
方法1. let fruit: IFruit = {
color: 'red',
taste: 'sweet',
b: 1,
c: 2
} as IFruit
方法2.interface IFruit {
color: string
taste: string
size?: number
[key: string]: any // 任意属性、可索引接口: 其他值是任意类型,名字随便写,key 的类型只能为 string(兼容所有值包括 Symbol) 或 number(key 为数字,常用于数组,叫可索引接口)
}
方法3. interface MyFruit extends IFruit { // 继承拓展
b: number
c: number
}
let fruit: MyFruit
方法4. 重载 interface IFuit { // 同名接口会合并,合并成一个接口
b?: number
c?: number
}
方法5. let fruit2 = {
color: 'red'
taste: 'sweet',
b: 1,
c: 2
}
let fruit2: IFruit = fruit3 // 满足了安全性,多的可以赋予少的
因为 fruit2 上有 b 和 c,而 IFruit 上没有,但满足其他条件,所以 fruit2 可以被定义为 IFruit,但 fruit3 也被定义为了 IFruit,因此不会得到 b 和 c 的类型提示
*/

接口继承

implements 可以传入一个列表,表示这个类要满足这个列表中类的方法,有具体的实现

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 Speakable {
speak(): void
}
interface SpeakChinese {
speakChinese(): void
}
class Speak implements Speakable, SpeakChinese {
speakChinese(): void {
throw new Error("Method not implemented.");
}
speak(): void {
throw new Error("Method not implemented.");
}
/*
这样声明也可以
speakChinese!: () => void
speak!: () => void
*/
}
/*
题外,给类扩展属性可以用命名空间写,必须写在 class Speak 的下方
namespace Speak {
export const a = 1
}
Speak.a // 1
*/

构造函数类型

类只能描述实例,当我们要描述类本身的时候,以下面代码为例,

typeof Cat 可以取这个类的类型,注意,typeof 只能对已有的类型的类型,不能对 T 这种泛型取类型

new (name: string, age: number) => Cat 表示是一个构造函数,我们限制返回的实例,new() 表示当前是一个构造函数类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type MyType = new (name: string, age: number) => Cat

interface IMyType {
new (name: string, age: number): Cat
}

class Cat {
constructor(public name: string, public age: number) {}
}

function createInstance(clazz: typeof Cat, name: string, age: number) {
return new clazz(name, age)
}
/*
另外的写法
function createInstance(clazz: IMyType, name: string, age: number)
function createInstance(clazz: MyType, name: string, age: number)
*/


let r = createInstance(Cat, 'tom', 13)

泛型

泛型的特点,就是解决在定义的时候不能明确类型,只能在使用的时候才能明确类型的情况。

用到泛型的地方前面一定要定义泛型的这个变量名

引子

在上面的构造函数类型中,如果 createInstance 的 clazz 类型不确定时,需要动态传入类型,就用上泛型,否则仅会定义为 Cat,比如我想传入 Dog 时被定义为 Dog,传入 Cat 被定义为 Cat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type MyType<T> = new (name: string, age: number) => T
interfact IMyType<T> {
new (name: string, age: number): T
}
class Cat {
constructor(public name: string, public age: number) {}
}
class Dog {
constructor(public name: string, public age: number) {}
}
function createInstance<T>(clazz: MyType<T>, name: string, age: number) {
return new clazz(name, age)
}
const d1 = createInstance<Dog>(Dog, 'Speike'15)
const d2 = createInstance(Dog, 'Speike'15) // 自动根据 Dog 得到 T 的类型,推断出 Dog
const c1 = createInstance<Cat>(Cat, 'Tom'15)
const c1 = createInstance<Cat>(Cat, 'Tom'15) // 自动根据 Cat 得到 T 的类型,推断出 Dog
多个泛型
1
2
3
4
5
6
7
8
9
10
function swap<A, B>(tuple: [A, B]): [B, A] { // 交换元组类型
return [tuple[1], tuple [0]]
}
let result = swap(['a', 1])

// 函数标注写法
type TArray = <T, K>(tuple: [T, K]) => [K, T]
const swapArr: TArray = <T, K>(tuple: [T, K]): [K, T] => {
return [tuple[1], tuple[0]]
}
默认泛型

用默认值来规定泛型的不确定性,不传递参数默认值是 unknown

1
2
3
4
5
interface TStr<T = string> { 
name: T
}
type TStr2 = TStr
let name1: TStr2 = { name: 'dl' }

泛型形参的位置

在写类型时如果泛型定义在函数的前面(<T>(param1){}),表示的是执行的时候确定类型,如果放在接口或者 type 的后面(IFn<T>, TFn<T>),表示使用这个类型的时候就定义好了具体类型。

比较以下几种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 声明时直接确定类型 比如 fn: IFn<number>,就被确定为 number 了,由于这里的 forEach 前面也有个 <T> ,导致看起来整体是运行时确定类型,实际上过 fn 的话,是再声明时候就确定了。
interface IFn<T> {
(item: T, index: number): void
}
const forEach = <T>(arr: T[], fn: IFn<T>) => { // ts 兼容性,可以多写,下面不取这个参数,同 js
for (let i = 0; i < arr.length; i++) {
fn(arr[i], i)
}
}
forEach([1, 2, 3], function (item) {
console.log(item)
})

interface ICreateArray { // 泛型定义在外面,表示使用类型的时候确定类型
<T>(times: number,item: T): T[] // 写在里面表示执行的时候确定类型
}

const createArray: ICreateArray = <T>(timers: number, item: T): Array<T> => {
const result = []
for (let i = 0; i < times; i++) {
result.push(item)
}
return result
}

泛型约束

泛型必须包含某些属性,就是泛型约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function sum<T extends string>(a: T, b: T): T {
return (a + b) as T
}
// 实际上,上面这个函数是为了输入 string 返回 string,输入 number 返回 number,用函数重载比较合适,这样写有点强制举例约束情况的意味

// 这样写会在返回值的时候报错,因为是联合联系,不能判断到底是 string 还是 number
function sum<T extends (string | number)>(a: T, b: T) {
return (a + b) as T
}



function fish<T extends { kind: string }>(val: T) {}

fish({kind: '鱼'}) // 不报错
fish({a: 1}) // 报错,a 不符合 { kind: string }
fish({kind: '鱼', a: 1}) // 不报错

keyof

我们期望在一个对象(类型的意思)能获取对象(类型)中所有的属性,可以用 keyof 关键字。返回泛型中指定属性,是一个联合类型,并集。

1
2
3
function getVal(T extends object, K extends keyof T)(val: T, key: K) {}
getVal({a: 1, b:2}, 'b') // b 属于 ('a' | 'b') 中的一个,ts 校验通过
getVal({a: 1, b:2}, 'c') // c 不属于 a 与 b 中的一个,ts 校验不通过

特殊且常用: type r = keyof any // string | number | symbol 因为这三个都能作为 key。

typeof

取出变量的类型,和 js 中的 typeof 不同,ts 的typeof 得到的是类型,js 得到的是值。

1
2
let val = { a: 1, b: 2 }
type v = typeof val // 获取 val 这个变量的类型,得到 { a: number, b: number }

交叉类型

表示两者共有的部分

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
interface A {
handsome: string
type: number
}
interface B {
high: string
type: string
}
type AB = A & B
/*
AB = {
handsome: string,
high: string,
type: never // 因为同时满足 string 和 number 的类型不存在,所以不应该有这个值,为 never
}
*/
let person: AB = {
handsome: '帅',
high: '高',
type: 'abc' as never // 通过 never 强行告诉 ts,这个 type 的类型为 never 来通过校验,实际上不应该这样做。
}

function mixin<T extends object, K extends object>(o1: T, o2: K): T & K {
return {...o1, ...o2}
}
mixin({a: 1}, {b: 2, c: 3})
let r1 = mixin({a: 1}, {a: '1'})
type MR = typeof r1 // 实际上会出现问题,a 属性将会变成 never 类型

条件类型

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
interface Bird {
name: '鸟'
}
interface Fish {
name: '鱼'
}
interface Sky {
color: '蓝色'
}
interface Water {
color: '白色'
}

type MyType<T> = T extends Bird ? Sky : Water // 三元表达式
// ts 有一个条件分发的概念,当 T 为裸类型,联合类型会发生条件分发,就是一个一个看结果最后拼装起来,比如一下内容,先取出 Fish,判断后得到 Water,再取 Bird 得到 Sky,最后把两者合起来,就是 Water | Sky,顺序无关。而不会先计算 Fish | Bird 的类型再去计算得到结果。
type MyAni = MyType<Fish | Bird> // <Sky | Water> 结果也是联合类型

type MyAni2 = MyType<Fish & Bird> // Sky,虽然我们可能想到 Bird 与 Fish 相交后,name 为 never,但从大的概念来讲(& 的定义),结果是 Fish & Bird 既能满足 Fish,又能满足 Bird,所以为 Sky,而且不会发生分发,因为不是联合类型,不会变成 Sky & Water 的结果。
/*
实际上 never 也能赋给其他类型,比如可以把 never 赋给 string、number 等,仅仅是类型上来说,不是从值上。
*/

// 这里由于 {name: T} extends {name: Bird} 里的 T 不在最外层,不是裸类型,联合类型不具备分发功能,将会先取 |,得到结果后,判断是否 extends {name: Bird},因此不满足,结果为 Water,这种写法很少用。一般不会包裹泛型的。
type MyType2<T> = {name: T} extends {name: Bird} ? Sky : Water
type MyAni3 = MyType<Fish | Bird> // Water

// 因为 Fish | Bird 的会带入变成 {name: Fish | Bird},是个大类型,无法满足 extends {name: Bird} 这个小类型,因此条件不满足,写成 extends {name: Bird | Fish} 就能满足了

内置类型

都可以自己实现,以下为自行实现。前几个类型依赖条件类型。

Exclude

传入两个类型,从第一个类型中排除第二个类型后返回最后的类型。

在多的类型中去掉某个类型,可以考虑使用。

1
2
type Exclude<T, U> = T extends U ? never: T
type MyExclude = Exclude<string | number | boolean, number | string> // 触发了分发得到结果为 never | never | boolean,最后得到 boolean

Extract

传入两个类型,从第一个类型选中中第二个类型中也存在的类型后返回。

1
2
type Extract<T, U> = T extends U ? T : never
type MyExtract = Extract<string | number | boolean, number | string> // 得到 number | string,与 Exclude 相反,同样也触发分发了

NonNullable

去除 null 和 undefined 类型。

1
2
3
4
5
6
7
let el = document.getElementById('app') 
type TEl = typeof el // HTMLElement | null
type MyNon = NonNullable<TEl> // HTMLElement

// 两种实现都可以
type NonNullable<T> = Exclude<T, null | undefined>
type NonNullable<T> = T extends null | undefined ? never : T

Partial

让外层属性变成可选属性。 ts 中也有 key in keys 这样的循环。

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
interface ICompany {
name: string,
address: string
}

interface IPerson {
name: string,
age: number,
company: ICompany
}

type Partial<T> = { [K in keyof T]?: T[K] } // 循环对象, K in keys
type MyPartial = Partial<IPerson>
/*
内置的 Partial 只解析一层,得到的结果为
{
name?: string,
age?: number,
company: ICompany
}
*/
// 自行实现多层级的 Partial
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }
let MyPartial2 = DeepPartial<IPerson>
/*
递归解析所有层数,得到的结果为
{
name?: string,
age?: number,
company?: {
name?: string,
address?: string
}
}
*/

Required

让外层属性变成必填项。- 是 ts 的操作符,可以去掉一些操作符。

1
2
3
4
5
type Required<T> = { [K in keyof T]-?: T[K] } // 和 Partial 实现基本一致
interface IP{
name?: string
}
type IPRequired = Required<IP> // { name?: string }

Readonly

让外层属性变成仅读项。- 是 ts 的操作符,可以去掉一些操作符。

1
2
3
4
5
6
7
8
9
type Readonly<T> = { readonly [K in keyof T]: T[K] } // 和 Partial 实现基本一致
interface IP{
name: string
}
type IPRequired = Readonly<IP> // { readonly name: string }

// 手动实现去除 readonly
type RemoveReadonly<T> = { -readonly [K in keyof T]: T[K] }
type IP2 = RemoveReadonly<Readonly<IP>> // { name: string }

Pick

挑选出想要的结果,传入两个类型,第一个类型为需要被挑选的接口(对象),第二个为要被挑选的接口上的属性。

1
2
3
4
5
6
7
interface IPerson {
name: string,
age: number,
address: string
}
type Pick<T, K extends keyof T> = { [P in K]: T[P] } // 不用 [P in keyof K], 因为 K 已经是 'name' | 'age' 这种 key 值了
type MyPerson = Pick<IPerson, 'name' | 'age'> // { name: string, age: number }

Omit

忽略属性,传入两个类型,第一个类型为需要被忽略的接口(对象),第二个为要被忽略的属性(可以写不是第一个接口上的属性,这和 Pick 不同)。

1
2
3
4
5
6
7
8
9
interface IPerson {
name: string,
age: number,
address: string
}
// keyof any ,上面有提到过,得到 string | number | symbol
// 通过 Exclude 排除掉不需要的 key,得到需要的 key,再通过 Pick 选出这些需要的 key
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
type MyPerson2 = Omit<IPerson, 'name' | 'age'> // { address: string }

Record

可以描述对象类型,是键值对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Record<T extends keyof any, K> = {
[P in K]: K // 很明显,P 是 string, number, symbol 中的一种
}

// 可以方便地描述键值对
let obj: Record<string, number | string> = { name: 'dl', age: 12 }

// 给一个对象,转化为另一个对象,传入 Record<T, K>,返回 <Record<T, k>,这样返回值会有推断功能,比如 obj2 可以推断出来返回值是 Record<string, number>
function map<T extends keyof any, K, U>(obj: Record<T, K>, fn: (item: K, index: T) => U): Record<T, U> {
let result = {}
for (let key in obj) {
result[key] = fn(obj[key], key)
}
return result
}

let obj2 = map({ name: 'dl', age: 12 }, function (item, key) {
return 123
})

ReturnType

推断函数的返回值。 infer 在哪里就推断哪里的结果,下面的 infer 在返回值的地方,因此推断返回值。

1
2
3
4
5
6
7
8
9
10
function getPoint(x: string, y: string, z: string) {
return { x, y, z }
}
// 如何得到 getPoint 的返回值?
// 下面代码执行后可以得到 res 类型为 { x: string, y: string, z: string },但我们希望不执行函数就得到返回值,需要推断
let res = getPoint('a', 'b', 'c')

// T extends (...args: any[]) => any,表示是函数类型,后面这个 extends 表示最大的函数范围
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any // T extends (...args: any[]) => infer R 相当于定义了 R 这个变量,不能写 = (...args: any[]) => infer R,因为这不是类型。
type MyReturn = ReturnType<typeof getPoint>

Paramaters

推断属性。看看 infer 在属性位置,和上面的 returntype 对比一下,大概就能知道 infer 的用法了。

1
2
3
4
5
function getPoint(x: string, y: string, z: string) {
return { x, y, z }
}
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer R) => any ? R : any
type MyParamaters = Parameters<typeof getPoint>

ConstructorParameters

推断类上的属性。同理,用 infer 来推断,不一样的是,我们需要用 new 来标志这个类来 extends,和用上面的函数来 extends 一样。

1
2
3
4
5
6
7
8
9
10
class Person {
constructor(name: string, age: number)
}

// 两种写法都可以
// 1. extends interface
type ConstructorParameters<T extends { new (...args: any[]): any}> = T extends { new (...agrs: infer R): any } ? R : never
// 2. extends type
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...agrs: infer R) => any ? R : never
type MyConstructor = ConstructorParameters<typeof Person>

InstanceType

推断实例,好像没什么用,要实例的类型,不就是类的类型吗。。

1
2
3
type InstanceType<T extends { new (...args: any[]): any }> = T extends { new (...args: any[]) => infer R } ? R : never
type MyInstanceType = InstanceType<typeof Person> // Person
type MyInstanceType = Person // 要实例的类型,不就是类的类型吗。。 和上面的一样

unknown

unknown 可以认为 是 any 的安全类型。

  • unknown 不能进行取值操作,

  • 和任何类型做联合类型,都是 unknown,

  • 和其他类型做交集得到的是其他类型,

  • 不支持 keyof unknown(即 unknown 类型不能被遍历)

  • never 是 unknown 的子类型 never extends unknown

  • keyof unknown 是never

1
2
3
4
let a: unknown = { a: 1 }
a.a // 报错

type x = never extends unknown ? true ? false // true

交、叉、并集

自行实现交、叉、并集和展开类型(Compute),Diff,Overwrite。

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
const person1 = {
name: 'dl',
age: 12,
address: 'CN'
}
const person2 = {
name: 'dl2'
}
type Person1 = typeof person1
type Person2 = typeof person2

// 求差集
type Diff<T extends object, K extends object> = Omit<T, keyof K>
type MyDiff = Diff<Person1, Person2>

// 求交集,从前一个中取出与后一个类型中相同的类型
type InterSection<T extends object, K extends object> = Pick<T, Extract<keyof T, keyof K>>
type MyInter = InterSection<Person1, Person2>


const person3 = {
name: 'dl',
age: 12,
address: 'CN'
}
const person4 = {
name: 'dl2',
age: '12'
}
type Person3 = typeof person3
type Person4 = typeof person4

// 求并集 T & K 如果有同名字段类型不一致会出现 unknown,封装一个类型 Diff,并集以后者为主
type Merge<T extends object, K extends object> = Omit<T, keyof K> & K
type NewPerson = Merge<Person3, Person4>

/* 上面的 NewPerson 会得到类型
type NewPerson = Omit<{
name: string
age: number
address: string
}, 'name' | 'age'> & {
name: string
age: number
}
看起来并不清楚,因此我们实现一个 Compute 类,展开所有的类型,可以看的更清楚
相当于帮别人给你的一堆百元零钱整合到了一张百元纸钞
*/
type Compute<T> = { [K in keyof T]: T[K] }
type NewPerson2 = Compute<NewPerson> // { name: string, age: number, address: string }

const person5 = {
age: 12,
address: 'CN'
}
const person6 = {
name: 'dl2',
age: '12'
}
type Person5 = typeof person5
type Person6 = typeof person6

// 重写类型,后者同名类型覆盖掉前者类型,不新增前者类型,仅不同时修改。
// type Overwrite<T extends object, K extends object> = Omit<T, keyof K> & Pick<K, keyof K> // 这样写是不行的,因为 Pick<T, keyof K> 中,会选中 T 中的 name 属性,而 T 并没有这个属性,报错,因此需要先找到共同属性,但 Omit 不会报错

// type Overwrite<T extends object, K extends object> = Omit<T, Extract<keyof T, keyof K>> & Pick<K, Extract<keyof T, keyof K>> // 由于 Omit 查找不存在类型不会报错,可以简写为
type Overwrite<T extends object, K extends object> = Omit<T, keyof K> & Pick<K, Extract<keyof T, keyof K>>

// 如果利用上面已经完成的类型,可以写成
type Overwrite<T extends object, K extends object> = Omit<T, keyof K> & InterSection<K, T>
type MyOverwrite = Overwrite<Person5, Person6>

类型保护

类型保护能让 ts 更好的去识别类型,js 本身就有类型识别功能,如 typeof、instanceof、in、?,可以用于类型保护,当我们使用 js 的这些关键词的时候,ts 会自动识别并进行类型保护。

js 中 typeof、instanceof、in、? 以及 ts 中 is 的保护

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
// typeof
function getV(val: string | number) {
if (typeof val === 'string') {
val.split('') // 判断为 string,调用 string 类型方法
} else {
val.toFixed() // 判断为 number,调用 number 类型方法
}
}

class Person {}
class Animal {}
// instanceof
function getV1(val: Person | Animal) {
if (val instanceof Person) {
val // Person
} else {
val // Animal
}
}

interface Fish {
swimming: string
}
interface Bird {
fly: string
}
// in
function getV2(val: Fish | Bird) {
if ('swimming' in val) {
val // Fish
} else {
val // Bird
}
}

interface Fish2 {
kind: '鱼'
swimming: string
}
interface Bird2 {
kind: '鸟'
swimming: string
}
function getV3(val: Fish2 | Bird2) {
// 设想一下,如果 Bird2 与 Fish2 中字段相同,就不能用这个来判断了
if ('swimming' in val) {
val // Fish2 | Bird2
} else {
val // never
}
// 在 interface 上新增 kind 属性来判断。
if (val.kind === '鱼') {
val // Fish2
} else {
val // Bird2
}
}

function isFish(val: Fish2 | Bird2): boolean {
return val.kind === '鱼'
}
function isFish2(val: Fish2 | Bird2): val is Fish { // is 只能用于布尔值
return val.kind === '鱼'
}

function getV4(val: Fish2 | Bird2) {
// 由于每次都 val.kind === ’鱼‘ 可能让用户觉得很烦,所以封装了一个函数返回布尔值,但是 ts 并不能根据布尔值来确定类型,因为 ts 是静态的,不会运行代码,所以它不知道你封装的方法里判断了 val.kind === '鱼'
if (isFish(val)) {
val // Fish2 | Bird2
} else {
val // Fish2 | Bird2
}

// 通过 is 关键词,指定返回 true 时,val 的类型为 Fish,相当告诉 ts 返回的布尔值的意义,因此 ts 能够判断类型了。
if (isFish2(val)) {
val // Fish2
} else {
val // Bird2
}
}

a?.() // null 保护

完整性保护

never 的用法有一个很经典的例子,有点像 C++ 中的断言,当状态判断情况缺少的时候就会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface ISquare {
kind: 'square'
}
interface IRant {
kind: 'rant'
}
interface ICircle {
kind: 'circle'
}
function assert(obj: never) {}
function getType(val: ISquare | IRant | ICircle) {
switch (val.kind) {
case 'square':
break
case 'rant':
break
// case 'circle': // 解除注释就不会报错
// break
default:
assert(val) // 由于这里少标注了 circle 的情况,这里就会报错,因为传入的 val 不是 never 类型,可以起到提示用户状态判断少的作用。
}
}

ts 兼容性

鸭子类型检测:范围小的可以赋予范围大的,范围大的不能赋予范围小的,只要满足类型条件即可,不关注内容。

接口类型兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let v1!: string | number
let v2!: string | number | boolean
v1 = v2 // error 范围大的不能赋予范围小的
v2 = v1 // 范围小的可以赋予范围大的

interface A1 {
name: string
age: number
}
interface A2 {
name: string
age: number
address: string
}
let a1!: A1
let a2!: A2
a2 = a1 // error 范围大的不能赋予范围小的,a1 上可以没有 address 属性,而 a2 必须有
a1 = a2 // 范围小的可以赋予范围大的

函数兼容

参数和返回值都兼容情况下才不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 参数兼容
let sum1 = (a: string, b: string) => {}
let sum2 = (a: string) => {} // 参数少的范围更大,相当于后面的参数是 ...args: any[]
sum2 = sum1 // error 范围大的不能赋予范围小的
sum1 = sum2 // 范围小的可以赋予范围大的

// 返回值兼容
let sum3 = (a: string, b: string) => ({address: string})
let sum4 = (a: string) => ({name: 'dl', age: 10})
sum1 = sum4 // 范围小的可以赋予范围大的, {name: 'dl', age: 10} 可以赋值给 {},个人感觉 {} 可以视作 Record<keyof any, any>
sum3 = sum4 // error 少了 address

let sum5 = (a: string, b: string): string | number => 1
let sum6 = (a: string): string => ''
sum5 = sum6 // 范围小的可以赋予范围大的,sum6 仅为 string,sum5 可以为 string 和 number

类的类型兼容

鸭子检测,只要实例属性类型一样,那就是一个东西(不会检测原型链,因为这是在检测值而不是类型,ts 仅检测类型)

特殊情况是,如果类中带了 private、protected 等修饰符,就不是一个东西了,不再兼容(默认属性是 public 可以兼容)。

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
class Person {}
class Animal {}

let p!: Person
let a!: Animal
p = a // 正确
a = p // 正确


class Person2 {
pubilc name="张三"
}
class Animal2 {

}

let p2!: Person2
let a2!: Animal2
p2 = a2 // error,范围大的不能赋予小的,因为 a2 上可以没有 name 属性,而 p2 上必须要有
a2 = p2 // 正确

class Person3 {
private name="张三"
}
class Animal3 {
private name="张三"
}

let p3!: Person3
let a3!: Animal3
// 如果都改为 public 是可以兼容的
p3 = a3 // error private 不兼容
a3 = p3 // error private 不兼容

枚举兼容

枚举永远不兼容

1
2
3
4
5
enum E1 {}
enum E2 {}
let e1!: E1
let e2!: E2
e1 == e2 // error 枚举永远不兼容

泛型兼容

泛型是否兼容看最终的结果

1
2
3
4
5
interface I1<T> {}
interface I2<T> {}
let i1!: I1<string>
let i2!: I2<number>
i1 = i2 // 只看最终结果,这里都为 {}, 可以兼容

逆变和协变

逆变和协变是针对函数(传入参数为函数的高级函数)的抽象概念,主要是参数与返回值的父子关系。

可以用来记忆函数的类型兼容性。

参数是逆变的,返回值是协变的,可以返回儿子(总结就是传父返子,即需要传入大范围的参数返回小范围的结果的函数)。

如果配置 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
class GrandParent {
house!: string
}
class Parent extends GrandParent {
money!: string
}
class Son extends Parent {
play!: string
}
function getFn(cb: (val: Parent) => void) {}
getFn(() => {}) // (val: Parent) => void 可以接受 () => void,因为后者范围更大
getFn((val: GrandParent) => {}) // (val: Parent) => void 可以接受 (val: GrandParent) => void,因为后者范围更小
getFn((val: Parent) => {}) // (val: Parent) => void 可以接受 (val: Parent) => void,因为范围一样大
getFn((val: Son) => {}) // error (val: Parent) => void 不能接受 (val: Son) => void,因为后者范围更大
// 相当于把传入的参数类型赋予到定义的参数类型上
/*
相当于
let f1!: () => void
let f2!: (val: GrandParent) => void
let f3!: (val: Parent) => void
let f4!: (val: Son) => void
f3 = f1
f3 = f2
f3 = f3
f3 = f4 // error
最后相当于
let t3!: Parent
let t4!: Son
t4 = t3 // 注意这里和返回值位置相反,大范围的不能赋予小范围的

举例子,比如说 (val: Son) => void 方法中可能调用了 Son.play 这个属性,而 (val: Parent) => void 中没有,就会出问题
比如
function getFn1(cb: (val: number | boolean) => void) { cb(true); cb(1) } // 调用了 true 和 1, 所以下面传入一定要 boolean 类型和 number 类型。
getFn1((val: number| boolean | string) => {}) // 大范围的 (val: number | boolean) => void) 可以接受 (val: number| boolean | string) => viod 小范围的。
*/
function getFn2(cb: (val: Parent) => Parent) {}
getFn((val: GrandParent) => new Son) // 不会报错,因为参数是符合规则的,对比返回值,Parent 范围比 Son 大。所以不报错
/*
相当于
let f5!: (val: Parent) => Parent
let f6!: (val: GrandParent) => Son
f5 = f6 // 永远是定义的类型在左边,自己要赋值的类型在右边去记就好
最后相当于
let t5!: Parent
let t6!: Son
t5 = t6 // 大范围可以接受小范围,注意这里和参数位置
*/

命名空间

为了解决命名冲突问题,

命名合并问题,能合并的东西有接口同名可以合并,函数和命名空间可以合并,命名空间和命名空间可以合并,命名空间和类可以合并,(类还可以和接口合并)

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
namespace Zoo1 {
export let m = '猴子'
}
namespace Zoo2 {
export let m = '猴子'
}
namespace Zoo3 { // 没有编译结果,因为没有 export
let m = '猴子'
}
/*
编译后结果为
(function (Zoo1) {
Zoo1.m = '猴子'
})(Zoo1 || (Zoo1 = {})) // 如果有 Zoo1 冲突了这样就能解决了
(function (Zoo2) {
Zoo2.m = '猴子'
})(Zoo2 || (Zoo2 = {}))
*/

// namespace.ts 一般会写成
export namespace Zoo3 {
export let m = '猴子'
}
// 另一个文件
import { Zoo3 } from './namespce'
console.log(Zoo3)

function Fn() {}
namespace Fn { // 给函数扩展属性,就可以不用 interface 了
export let a = 1
export let b = 2
}

// 接口、类、命名空间合并
interface Person { // 给类原型(Person.prototype)扩展属性
say(): void
}
class Person {
// say!: () => void 也是给类原型(Person.prototype)扩展属性
}
namespace Person {
export function say() {} // 给类本身扩展属性
}

declare

declare 是声明的意思。

如果是全局的变量扩展,需要使用 declare global,否则无法访问

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
// 无效
interface Window {
xxx: string
}
window.xxx = 1// window.xxx 不存在,赋值失败

// 告诉编译器我定义的是全局的 window
declare global { // 接口的合并
interface Window {
xxx: string
}
}
window.xxx = 1 // 存在,并赋值成功

declare global { // 接口的合并
interface String {
xxx(): void
}
}
String.prototype.xxx = () => {} // 赋值成功

// 如果 jq 是 cdn 引入的,那么当前模块是不会引入 jq 的
// 用来声明 jq 的类型
declare function $(selector: string): { // 一般写在 .d.ts 的声明文件中,不是正常引入的类型的声明全都使用 declare 关键字,只是为了有提示,不会打包。
css(val: string): void
}
declare namespace $ { // declare 中的属性默认是导出的,不需要 export
namespace fn { // $.fn
function extend(): void
}
}
$('').css('xxx')
$.fn.extend()

声明文件

ts 下安装第三方包,必须有第三方包的类型声明,否则自己添加一个包含 declare module thirdPackage 的 .d.ts 文件。

很多第三方包都已经有人写好了声明文件,我们只需要引入即可 npm i @types/jquery -S .

当前代码会默认先查找 node_modules 下的某个模块的 package.json 中是否有 types 字段,如果没有,会去找目录下的 index.d.ts,如果也没有,就会去找 node_modules/@types 下的文件(比如我要用 jq,就是找 @types/jquery),默认去找 index.d.ts 文件。

自定义模块声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
declare module jquery // 写在 typings/index.d.ts 或其他 .d.ts
declare module '*.png' // 解析 png 文件,写在 types/index.d.ts 或其他 .d.ts
declare module '*.vue' // 解析 .vue 文件,写在 types/index.d.ts 或其他 .d.ts

declare module '*.vue' {
interface Vue {
install: () => void
}
const vue: Vue
export = vue
}
// 其他文件
import component from 'a.vue' // 引入后自动有了类型
component.install
自定义的声明路径

ts 的 declare 一般情况下,第三方模块写在根目录下的 typings/index.d.ts 中,相对路径的写到根目录下的 types/index.d.ts 中,都需要对 ts.config.json 做配置。

  1. 第三方模块需要在 compilerOptions 同级属性下新增 "include": ["typings/**/*"]**/* 表示该目录所有深度下的 .d.ts 都会被读取。
  2. 相对路径的引入需要配置 compilerOptions 属性中的 baseUrl: "./"(告诉 ts 以哪个目录解析)、paths:{ "*": ["types/*"] },设置完这个写自己的类型就可以不用在模块里到处 import type {xxx} from 'xxx' 了,已经自动引入了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"compilerOptions": {
// "declaration": true, /* 打包后自动生成声明文件 */
// "moduleResolution": "node", /* 告诉TypeScript编译器,采用何种方式解析(也就是查找)TypeScript文件中依赖的模块的位置,可选项为:Classic 和 Node,比如 jq 没有实现 export 这种导出,我们就需要将这个值设置为 node */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": {
"*": [
"types/*"
]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "types": [], /* Type declaration files to be included in compilation. */
},
"include": [
"typings/**/*"
]
}
兼容 commonJS

因为在 ts 中,使用 module.exports = xxx 或 module = xxx 或 require 都会使得类型变成 any,因此ts 中为了兼容 commonJS,存在特殊写法。(ts 中使用 import 和 export 会自动推导类型)

1
2
3
4
5
6
7
8
9
// 2.ts
export = {
a: 1,
b: 2
}
// 1.ts
import a = require('./2')
a.a // 存在
a.b // 存在

/// 指令

很老的写法,三斜杠指令表示打包的时候引入这些声明。

1
2
/// <reference path="JQuery.d.ts"> 
export = JQuery // 这个 JQuery 定义在上面的文件,但通过 /// 已经引入到这个文件夹里了,相当于已经声明了

declaration

在 ts.config.json 中有 declaration 属性,将其设置为 true,在打包后会根据类型自动生成 .d.ts 文件

1
2
3
4
5
let a: string = '123'
export { a }
// 自动生成 src/index.d.ts
declare let a: string
export { a }
分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理