# 一份通俗易懂的 TS 教程,入门 + 实战

返回:ts

TypeScript JavaScript
JavaScript 的超集,用于解决大型项目的代码复杂性 一种脚本语言,用于创建动态网页。
强类型,支持静态和动态类型 动态弱类型语言
可以在编译期间发现并纠正错误 只能在运行时发现错误
不允许改变变量的数据类型 变量可以被赋予不同类型的值

# 基础类型

# boolean、number 和 string 类型

# undefined 和 null 类型

默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。

# any、unknown 和 void 类型

  • any
    • 不清楚用什么类型,可以使用 any 类型。这些值可能来自于动态的内容,比如来自用户输入或第三方代码库
    • 不建议使用 any,不然就丧失了 TS 的意义
  • unknown 类型(代表任何类型)
    • 不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?可以使用 unknown 类型
    • unknown 类型代表任何类型,它的定义和 any 定义很像,但是它是一个安全类型,使用 unknown 做任何事情都是不合法的。

比如,这样一个 divide 函数,

function divide(param: any) {
  return param / 2;
}
1
2
3

把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。

把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险,如下图

function divide(param: unknown) {
  return param / 2;
}
1
2
3

因为不知道 param 的类型,使用运算符 /,导致报错

再配合类型断言,即可解决这个问题,

function divide(param: unknown) {
  return param as number / 2;
}
1
2
3
  • void
    • void 类型与 any 类型相反,它表示没有任何类型。

# never 类型

类型表示的是那些永不存在的值的类型。

never 类型是任何类型的子类型,也可以赋值给任何类型。
没有类型是 never 的子类型,没有类型可以赋值给 never 类型(除了 never 本身之外)。即使 any 也不可以赋值给 never 。

  • 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行。
  • 函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步。

# 数组类型

2022-03-10_165300.gif

  • 数组里的项写错类型会报错
  • push 时类型对不上会报错

如果数组想每一项放入不同数据怎么办?用元组类型

# 元组类型

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let tuple: [number, string] = [18, "lin"];
1
  • 写错类型会报错
  • 越界会报错
  • 可以对元组使用数组的方法,比如使用 push 时,不会有越界报错

# 函数类型

定义函数类型需要定义输入参数类型输出类型(输出类型也可以忽略,因为 TS 能够根据返回语句自动推断出返回值类型。)。

# 函数表达式写法

let add2 = (x: number, y: number): number => {
  return x + y;
};
1
2
3

# 可选参数

参数后加个问号,代表这个参数是可选的

WARNING

注意可选参数要放在函数入参的最后面,不然会导致编译错误。

function add(x: number, y: number, z?: number): number {
  return x + y;
}

add(1, 2, 3);
add(1, 2);
1
2
3
4
5
6

# 函数赋值

JS 中变量随便赋值没问题,

let fn = functoin() {}

fn = '123'
1
2
3

# 函数重载

# 不同参数类型

比如我们实现一个 add 函数,如果传入参数都是数字,就返回数字相加,如果传入参数都是字符串,就返回字符串拼接,

function add(x: number[]): number
function add(x: string[]): string
function add(x: any[]): any {
  if (typeof x[0] === 'string') {
    return x.join()
  }
  if (typeof x[0] === 'number') {
      return x.reduce((acc, cur) => acc + cur)
  }
}
1
2
3
4
5
6
7
8
9
10

在 TS 中,实现函数重载,需要多次声明这个函数,前几次是函数定义,列出所有的情况,最后一次是函数实现,需要比较宽泛的类型,比如上面的例子就用到了 any。

# 不同参数个数

假设这个 add 函数接受更多的参数个数,比如还可以传入一个参数 y,如果传了 y,就把 y 也加上或拼接上,就可以这么写,

function add(x: number[]): number
function add(x: string[]): string
function add(x: number[], y: number[]): number
function add(x: string[], y: string[]): string
function add(x: any[], y?: any[]): any {
  if (Array.isArray(y) && typeof y[0] === 'number') {
      return x.reduce((acc, cur) => acc + cur) + y.reduce((acc, cur) => acc + cur)
  }
  if (Array.isArray(y) && typeof y[0] === 'string') {
      return x.join() + ',' + y.join()
  }
  if (typeof x[0] === 'string') {
    return x.join()
  }
  if (typeof x[0] === 'number') {
      return x.reduce((acc, cur) => acc + cur)
  }
}


console.log(add([1,2,3]))      // 6
console.log(add(['lin', '18']))  // 'lin,18'
console.log(add([1,2,3], [1,2,3])) // 12
console.log(add(['lin', '18'], ['man', 'handsome'])) // 'lin,18,man,handsome'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

其实写起来挺麻烦的,后面了解泛型之后写起来会简洁一些,不必太纠结函数重载,知道有这个概念即可,平时一般用泛型来解决类似问题。

# interface

# interface 基本概念

interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。

定义 interface 一般首字母大写,代码如下

interface Person {
    name: string
    age: number
}

const p1: Person = {
    name: 'lin',
    age: 18
}
1
2
3
4
5
6
7
8
9
  • 属性必须和类型定义的时候完全一致
    • 少写了属性,报错:
    • 多写了属性,报错:

interface

interface 不是 JS 中的关键字,所以 TS 编译成 JS 之后,这些 interface 是不会被转换过去的,都会被删除掉,interface 只是在 TS 中用来做静态检查。

# 可选属性

跟函数的可选参数是类似的,在属性上加个 ?,这个属性就是可选的,比如下面的 age 属性

interface Person {
    name: string
    age?: number
}

const p1: Person = {
    name: 'lin',
}
1
2
3
4
5
6
7
8

# 只读属性

如果希望某个属性不被改变,可以这么写:

interface Person {
    readonly id: number
    name: string
    age: number
}
1
2
3
4
5

改变这个只读属性时会报错

# interface 描述函数类型

interface 也可以用来描述函数类型

interface ISum {
  (x: number, y: number): number;
}

const add: ISum = (num1, num2) => {
  return num1 + num2;
};
1
2
3
4
5
6
7

# 自定义属性(可索引的类型)

上文中,属性必须和类型定义的时候完全一致,如果一个对象上有多个不确定的属性,怎么办?

interface RandomKey {
  [propName: string]: string;
}

const obj: RandomKey = {
  a: "hello",
  b: "lin",
  c: "welcome",
};
1
2
3
4
5
6
7
8
9

如果把属性名定义为 number 类型,就是一个类数组了,看上去和数组一模一样

interface LikeArray {
  [propName: number]: string;
}

const arr: LikeArray = ["hello", "lin"];

arr[0]; // 可以使用下标来访问值
1
2
3
4
5
6
7

当然,不是真的数组,数组上的方法它是没有的

# duck typing

interface 还有一个响亮的名称:duck typing(鸭子类型)。

TIP

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
-- James Whitcomb Riley

interface FunctionWithProps {
    (x: number): number
    name: string
}
1
2
3
4

FunctionWithProps 接口描述了一个函数类型,还向这个函数类型添加了 name 属性,这看上去完全是四不像,但是这个定义是完全可以工作的。

const fn: FunctionWithProps = (x) => {
  return x;
};

fn.name = "hello world";
1
2
3
4
5

#

JS 是靠原型和原型链来实现面向对象编程的,es6 新增了语法糖 class。

TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。

# 类基本写法

定义一个 Person 类,有属性 name 和 方法 speak

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} is speaking`);
  }
}

const p1 = new Person("lin"); // 新建实例

p1.name; // 访问属性和方法
p1.speak();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 继承

使用 extends 关键字实现继承,定义一个 Student 类继承自 Person 类。

class Student extends Person {
  study() {
    console.log(`${this.name} needs study`);
  }
}

const s1 = new Student("lin");

s1.study();
1
2
3
4
5
6
7
8
9

继承之后,Student 类上的实例可以访问 Person 类上的属性和方法。

# super 关键字

注意,上例中 Student 类没有定义自己的属性,可以不写 super ,但是如果 Student 类有自己的属性,就要用到 super 关键字来把父类的属性继承过来。

比如,Student 类新增一个 grade(成绩) 属性,就要这么写:

class Student extends Person {
  grade: number;
  constructor(name: string, grade: number) {
    super(name);
    this.grade = grade;
  }
}

const s1 = new Student("lin", 100);
1
2
3
4
5
6
7
8
9

不写 super 会报错

# 多态

子类对父类的方法进行了重写,子类和父类调同一个方法时会不一样。

class Student extends Person {
  speak() {
    return `Student ${super.speak()}`;
  }
}
1
2
3
4
5

# public

public,公有的,一个类里默认所有的方法和属性都是 public。

比如上文中定义的 Person 类,其实是这样的:

class Person {
    public name: string
    public constructor(name: string) {
        this.name = name
    }
    public speak() {
        console.log(`${this.name} is speaking`)
    }
}
1
2
3
4
5
6
7
8
9

public 可写可不写,不写默认也是 public。

# private

private,私有的,只属于这个类自己,它的实例和继承它的子类都访问不到。

将 Person 类的 name 属性改为 private。

class Person {
    private name: string
    public constructor(name: string) {
        this.name = name
    }
    public speak() {
        console.log(`${this.name} is speaking`)
    }
}
1
2
3
4
5
6
7
8
9

实例访问 name 属性,会报错:

# protected

protected 受保护的,继承它的子类可以访问,实例不能访问。

将 Person 类的 name 属性改为 protected。

class Person {
    protected name: string
    public constructor(name: string) {
        this.name = name
    }
    public speak() {
        console.log(`${this.name} is speaking`)
    }
}
1
2
3
4
5
6
7
8
9

# static

static 是静态属性,可以理解为是类上的一些常量,实例和子类都不能访问。

比如一个 Circle 类,圆周率是 3.14,可以直接定义一个静态属性。

class Circle {
    static pi: 3.14
    public radius: number
    public constructor(radius: number) {
        this.radius = radius
    }
    public calcLength() {
        return Circle.pi * this.radius * 2  // 计算周长,直接访问 Circle.pi
    }
}
1
2
3
4
5
6
7
8
9
10

# 抽象类

抽象类,听名字似乎是非常难理解的概念,但其实非常简单。

TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。

其实 TS 还对 JS 扩展了一个新概念——抽象类。

所谓抽象类,是指只能被继承,但不能被实例化的类,就这么简单。

  • 抽象类有两个特点:
    • 抽象类不允许被实例化
    • 抽象类中的抽象方法必须被子类实现
abstract class Animal {}

const a = new Animal(); // 报错
1
2
3

# 抽象类中的抽象方法必须被子类实现

abstract class Animal {
    constructor(name:string) {
        this.name = name
    }
    public name: string
    public abstract sayHi():void
}

class Dog extends Animal {
    constructor(name:string) {
        super(name)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

定义一个 Dog 类,继承自 Animal 类,但是却没有实现 Animal 类上的抽象方法 sayHi,报错,

正确的用法如下,

abstract class Animal {
    constructor(name:string) {
        this.name = name
    }
    public name: string
    public abstract sayHi():void
}

class Dog extends Animal {
    constructor(name:string) {
        super(name)
    }
    public sayHi() {
        console.log('wang')
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 抽象方法和多态

多态是面向对象的三大基本特征之一

多态指的是,父类定义一个抽象方法,在多个子类中有不同的实现,运行的时候不同的子类就对应不同的操作

# this 类型

# 链式调用

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

class StudyStep {
  step1() {
    console.log("listen");
    return this;
  }
  step2() {
    console.log("write");
    return this;
  }
}

const s = new StudyStep();

s.step1().step2(); // 链式调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 灵活调用子类父类方法

在继承的时候,this 可以表示父类型,也可以表示子类型

class StudyStep {
  step1() {
    console.log("listen");
    return this;
  }
  step2() {
    console.log("write");
    return this;
  }
}

class MyStudyStep extends StudyStep {
  next() {
    console.log("before done, study next!");
    return this;
  }
}

const m = new MyStudyStep();

m.step1()
  .next()
  .step2()
  .next(); // 父类型和子类型上的方法都可随意调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

# interface 和 class 的关系

interface 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。

interface 同样可以用来约束 class,要实现约束,需要用到 implements 关键字。

# implements

implements 是实现的意思,class 实现 interface。

interface MusicInterface {
  playMusic(): void;
}

class Cellphone implements MusicInterface {
  playMusic() {}
}
1
2
3
4
5
6
7

定义了约束后,class 必须要满足接口上的所有条件,不写就会报错

# 处理公共的属性和方法

手机还有打电话的功能,就可以这么做,Cellphone 类 implements 两个 interface。

# 约束构造函数和静态属性

使用 implements 只能约束类实例上的属性和方法,要约束构造函数和静态属性,需要这么写。

interface CircleStatic {
    new (radius: number): void
    pi: number
}

const Circle:CircleStatic = class Circle {
    static pi: 3.14
    public radius: number
    public constructor(radius: number) {
        this.radius = radius
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

未定义静态属性 pi,会报错

# 枚举

# 基本使用

enum Direction {
    Up,
    Down,
    Left,
    Right
}
1
2
3
4
5
6

枚举成员会被赋值为从 0 开始递增的数字,

console.log(Direction.Up); // 0
console.log(Direction.Down); // 1
console.log(Direction.Left); // 2
console.log(Direction.Right); // 3
1
2
3
4

枚举会对枚举值到枚举名进行反向映射,

console.log(Direction[0]); // Up
console.log(Direction[1]); // Down
console.log(Direction[2]); // Left
console.log(Direction[3]); // Right
1
2
3
4

如果枚举第一个元素赋有初始值,就会从初始值开始递增

enum Direction {
    Up = 6,
    Down,
    Left,
    Right
}

console.log(Direction.Up)        // 6
console.log(Direction.Down)      // 7
console.log(Direction.Left)      // 8
console.log(Direction.Right)     // 9
1
2
3
4
5
6
7
8
9
10
11

# 反向映射的原理

枚举是如何做到反向映射的呢,我们不妨来看一下被编译后的代码

var Direction;
(function(Direction) {
  Direction[(Direction["Up"] = 6)] = "Up";
  Direction[(Direction["Down"] = 7)] = "Down";
  Direction[(Direction["Left"] = 8)] = "Left";
  Direction[(Direction["Right"] = 9)] = "Right";
})(Direction || (Direction = {}));
1
2
3
4
5
6
7

# 手动赋值

enum ItemStatus {
    Buy = 1,
    Send,
    Receive
}

console.log(ItemStatus['Buy'])      // 1
console.log(ItemStatus['Send'])     // 2
console.log(ItemStatus['Receive'])  // 3
1
2
3
4
5
6
7
8
9

但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。

比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写,

enum ItemStatus {
    Buy = 100,
    Send = 20,
    Receive = 1
}

console.log(ItemStatus['Buy'])      // 100
console.log(ItemStatus['Send'])     // 20
console.log(ItemStatus['Receive'])  // 1
1
2
3
4
5
6
7
8
9

# 计算成员

枚举中的成员可以被计算,比如经典的使用位运算合并权限,可以这么写,

enum FileAccess {
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
}

console.log(FileAccess.Read)       // 2   -> 010
console.log(FileAccess.Write)      // 4   -> 100
console.log(FileAccess.ReadWrite)  // 6   -> 110
1
2
3
4
5
6
7
8
9

# 字符串枚举

字符串枚举的意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

const value = 'UP'
if (value === Direction.Up) {
    // do something
}
1
2
3
4
5
6
7
8
9
10
11

# 常量枚举

上文的例子,使用 const 来定义一个常量枚举

const enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

const value = 'UP'
if (value === Direction.Up) {
    // do something
}
1
2
3
4
5
6
7
8
9
10
11
12
13

编译出来的 JS 代码会简洁很多,提高了性能。

const value = "UP";
if (value === "UP" /* Up */) {
  // do something
}
1
2
3
4

不写 const 编译出来是这样的,

var Direction;
(function(Direction) {
  Direction["Up"] = "UP";
  Direction["Down"] = "DOWN";
  Direction["Left"] = "LEFT";
  Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
const value = "UP";
if (value === Direction.Up) {
  // do something
}
1
2
3
4
5
6
7
8
9
10
11

# 类型推论

# 定义时不赋值

let a;

a = 18;
a = "lin";
1
2
3
4

定义时不赋值,就会被 TS 自动推导成 any 类型,之后随便怎么赋值都不会报错。

# 初始化变量

因为赋值的时候赋的是一个字符串类型,所以 TS 自动推导出 userName 是 string 类型。

# 设置默认参数值

定义一个打印年龄的函数,默认值是 18

function printAge(num = 18) {
  console.log(num);
  return num;
}
1
2
3
4

那么 TS 会自动推导出 printAge 的入参类型,传错了类型会报错。

# 决定函数返回值

# 最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。比如,

let arr = [0, 1, null, "lin"];
1

# 内置类型

# JS 八种内置类型

let name: string = "lin";
let age: number = 18;
let isHandsome: boolean = true;
let u: undefined = undefined;
let n: null = null;
let obj: object = { name: "lin", age: 18 };
let big: bigint = 100n;
let sym: symbol = Symbol("lin");
1
2
3
4
5
6
7
8

# ECMAScript 的内置对象

比如,Array、Date、Error 等,

const nums: Array<number> = [1, 2, 3];

const date: Date = new Date();

const err: Error = new Error("Error!");

const reg: RegExp = /abc/;

Math.pow(2, 9);
1
2
3
4
5
6
7
8
9

# DOM 和 BOM

比如 HTMLElement、NodeList、MouseEvent 等

# 高级类型(一)

# 联合类型

如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。

例如,一个变量既支持 number 类型,又支持 string 类型,就可以这么写:

let num: number | string;

num = 8;
num = "eight";
1
2
3
4

联合类型大大提高了类型的可扩展性,但当 TS 不确定一个联合类型的变量到底是哪个类型的时候,只能访问他们共有的属性和方法。

比如这里就只能访问 number 类型和 string 类型共有的方法,如下图,

如果直接访问 length 属性,string 类型上有,number 类型上没有,就报错了

# 交叉类型

如果要对对象形状进行扩展,可以使用交叉类型 &

比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性,就可以这么写,

interface Person {
    name: string
    age: number
}

type Student = Person & { grade: number }
1
2
3
4
5
6

这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性,

TIP

联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。

# 类型别名(type)

就像我们项目中配置 alias,不用写相对路径就能很方便地引入文件

import componentA from '../../../../components/componentA/index.vue'
// 变成
import componentA from '@/components/componentA/index.vue
1
2
3

类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。

比如下面这个例子,getName 这个函数接收的参数可能是字符串,可能是函数,就可以这么写。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver; // 联合类型
function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}
1
2
3
4
5
6
7
8
9
10

TIP

类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string; // 基本类型

type arrItem = number | string; // 联合类型

const arr: arrItem[] = [1, "2", 3];

type Person = {
  name: Name,
};

type Student = Person & { grade: number }; // 交叉类型

type Teacher = Person & { major: string };

type StudentAndTeacherList = [Student, Teacher]; // 元组类型

const list: StudentAndTeacherList = [
  { name: "lin", grade: 100 },
  { name: "liu", major: "Chinese" },
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# type 和 interface 的区别

比如下面这个例子,可以用 type,也可以用 interface。

interface Person {
    name: string
    age: number
}

const person: Person = {
    name: 'lin',
    age: 18
}

type Person = {
    name: string
    age: number
}

const person: Person = {
    name: 'lin',
    age: 18
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 两者相同点:
    • 都可以定义一个对象或函数
    • 都允许继承
type addType = (num1: number, num2: number) => number;

interface addType {
  (num1: number, num2: number): number;
}
// 这两种写法都可以定义函数类型

const add: addType = (num1, num2) => {
  return num1 + num2;
};
1
2
3
4
5
6
7
8
9
10
// interface 继承 interface
interface Person {
  name: string;
}
interface Student extends Person {
  grade: number;
}

const person: Student = {
  name: "lin",
  grade: 100,
};

// type 继承 type
type Person = {
  name: string,
};
type Student = Person & { grade: number }; // 用交叉类型

// interface 继承 type
type Person = {
  name: string,
};

interface Student extends Person {
  grade: number;
}

// type 继承 interface
interface Person {
  name: string;
}

type Student = Person & { grade: 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
29
30
31
32
33
34

interface 使用 extends 实现继承, type 使用交叉类型实现继承

  • 两者不同点:
    • interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
    • type 是类型别名,用于给各种类型定义别名,让 TS 写起来更简洁、清晰。
    • type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行
    • interface 可以合并重复声明,type 不行
// 重复声明 type ,就报错了
interface Person {
  name: string;
}

interface Person {
  // 重复声明 interface,就合并了
  age: number;
}

const person: Person = {
  name: "lin",
  age: 18,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 报错
type Person = {
  name: string,
};

type Person = {
  // Duplicate identifier 'Person'
  age: number,
};

const person: Person = {
  name: "lin",
  age: 18,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 类型保护

// 如果有一个 getLength 函数,入参是联合类型 number | string,返回入参的 length,此时会报错

function getLength(arg: number | string): number {
  return arg.length;
}
1
2
3
4
5

改造一下

function getLength(arg: number | string): number {
  if (typeof arg === "string") {
    return arg.length;
  } else {
    return arg.toString().length;
  }
}
1
2
3
4
5
6
7

# 类型断言

类型断言语法:

as 类型
1

使用类型断言来告诉 TS,我(开发者)比你(编译器)更清楚这个参数是什么类型,你就别给我报错了

function getLength(arg: number | string): number {
    const str = arg as string
    if (str.length) {
        return str.length
    } else {
        const number = arg as number
        return number.toString().length
    }
}
1
2
3
4
5
6
7
8
9

TIP

注意,类型断言不是类型转换,把一个类型断言成联合类型中不存在的类型会报错。

// 报错
function getLength(arg: number | string): number {
    return (arg as number[]).length
}
1
2
3
4

# 字面量类型

有时候,我们需要定义一些常量,就需要用到字面量类型,比如,

type ButtonSize = "mini" | "small" | "normal" | "large";

type Sex = "男" | "女";

const sex: Sex = "不男不女";
1
2
3
4
5

# 泛型

TIP

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。

WARNING

写 any 类型不好,毕竟在 TS 中尽量不要写 any

# 泛型基本使用

泛型的语法是 <> 里写类型参数,一般可以用 T 来表示

function print<T>(arg: T): T {
  console.log(arg);
  return arg;
}
1
2
3
4

这样,我们就做到了输入和输出的类型统一,且可以输入输出任何类型。

TIP

泛型的写法对前端工程师来说是有些古怪,比如 <> T ,但记住就好,只要一看到 <>,就知道这是泛型。

print <string> "hello"; // 定义 T 为 string

print("hello");
1
2
3

我们知道,type 和 interface 都可以定义函数类型,也用泛型来写一下,type 这么写:

type Print = <T>(arg: T) => T;
const printFn: Print = function print(arg) {
  console.log(arg);
  return arg;
};
1
2
3
4
5

interface 这么写:

interface Iprint<T> {
  (arg: T): T;
}

function print<T>(arg: T) {
  console.log(arg);
  return arg;
}

const myPrint: Iprint<number> = print;
1
2
3
4
5
6
7
8
9
10

# 默认参数

如果要给泛型加默认参数,可以这么写:

interface Iprint<T = number> {
  (arg: T): T;
}

function print<T>(arg: T) {
  console.log(arg);
  return arg;
}

const myPrint: Iprint = print;
1
2
3
4
5
6
7
8
9
10

# 处理多个函数参数

现在有这么一个函数,传入一个只有两项的元组,交换元组的第 0 项和第 1 项,返回这个元组。

function swap(tuple) {
  return [tuple[1], tuple[0]];
}
1
2
3

这么写,我们就丧失了类型,用泛型来改造一下。

我们用 T 代表第 0 项的类型,用 U 代表第 1 项的类型。

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}
1
2
3

# 函数副作用操作

我们希望调用 API 都清晰的知道返回类型是什么数据结构,就可以这么做

interface UserInfo {
    name: string
    age: number
}

function request<T>(url:string): Promise<T> {
    return fetch(url).then(res => res.json())
}

request<UserInfo>('user/info').then(res =>{
    console.log(res)
})
1
2
3
4
5
6
7
8
9
10
11
12

# 约束泛型

假设现在有这么一个函数,打印传入参数的长度,我们这么写:

// 会报错
function printLength<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}
1
2
3
4
5

那么现在我想约束这个泛型,一定要有 length 属性,怎么办?

可以和 interface 结合,来约束类型。

interface ILength {
    length: number
}

function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}
1
2
3
4
5
6
7
8

这其中的关键就是 <T extends ILength>,让这个泛型继承接口 ILength,这样就能约束泛型。

我们定义的变量一定要有 length 属性,比如下面的 str、arr 和 obj,才可以通过 TS 编译。

const str = printLength("lin");
const arr = printLength([1, 2, 3]);
const obj = printLength({ length: 10 });
1
2
3

# 泛型的一些应用

使用泛型,可以在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。

# 泛型约束类

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,就可以这么写:

class Stack<T> {
    private data: T[] = []
    push(item:T) {
        return this.data.push(item)
    }
    pop():T | undefined {
        return this.data.pop()
    }
}
// 给 pop 方法定义 static 关键字,就报错了
1
2
3
4
5
6
7
8
9
10

在定义实例的时候写类型,比如,入栈和出栈都要是 number 类型,就这么写:

const s1 = new Stack<number>()
1

这样,入栈一个字符串就会报错

WARNING

特别注意的是,泛型无法约束类的静态成员

# 泛型约束接口

使用泛型,也可以对 interface 进行改造,让 interface 更灵活。

interface IKeyValue<T, U> {
    key: T
    value: U
}

const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}
1
2
3
4
5
6
7

# 泛型定义数组

定义一个数组,我们之前是这么写的:

const arr: number[] = [1, 2, 3];

// 现在这么写也可以:

const arr: Array<number> = [1, 2, 3];
1
2
3
4
5

泛型(Generics),从字面上理解,泛型就是一般的,广泛的。

# 高级类型(二)

# 索引类型

从对象中抽取一些属性的值,然后拼接成数组

const userInfo = {
  name: "lin",
  age: "18",
};

function getValues(userInfo: any, keys: string[]) {
  return keys.map((key) => userInfo[key]);
}

// 抽取指定属性的值
console.log(getValues(userInfo, ["name", "age"])); // ['lin', '18']
// 抽取obj中没有的属性:
console.log(getValues(userInfo, ["sex", "outlook"])); // [undefined, undefined]
1
2
3
4
5
6
7
8
9
10
11
12
13

虽然 obj 中并不包含 sex 和 outlook 属性,但 TS 编译器并未报错

此时使用 TS 索引类型,对这种情况做类型约束,实现动态属性的检查。

TIP

理解索引类型,需先理解 keyof(索引查询)T[K](索引访问) 和 extends (泛型约束)

# keyof(索引查询)

keyof 操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

interface IPerson {
  name: string;
  age: number;
}

type Test = keyof IPerson; // 'name' | 'age'
1
2
3
4
5
6

# T[K](索引访问)

T[K],表示接口 T 的属性 K 所代表的类型,

interface IPerson {
  name: string;
  age: number;
}

let type1:  IPerson['name'] // string
let type2:  IPerson['age']  // number
1
2
3
4
5
6
7

# extends (泛型约束)

T extends U,表示泛型变量可以通过继承某个类型,获得某些属性,之前讲过

# 检查动态属性

对索引类型的几个概念了解后,对 getValue 函数进行改造,实现对象上动态属性的检查

  • 定义泛型 T、K,用于约束 userInfo 和 keys
  • 为 K 增加一个泛型约束,使 K 继承 userInfo 的所有属性的联合类型, 即K extends keyof T
function getValues<T, K extends keyof T>(userInfo: T, keys: K[]): T[K][] {
    return keys.map(key => userInfo[key])
}
1
2
3

这样当我们指定不在对象里的属性时,就会报错

# 映射类型

TS 允许将一个类型映射成另外一个类型。

# in

介绍映射类型之前,先介绍一下 in 操作符,用来对联合类型实现遍历。

type Person = "name" | "school" | "major"

type Obj =  {
  [p in Person]: string
}
1
2
3
4
5

# Partial

Partial<T>将 T 的所有属性映射为可选的

interface IPerson {
    name: string
    age: number
}

let p1: IPerson = {
    name: 'lin',
    age: 18
}
1
2
3
4
5
6
7
8
9

使用了 IPerson 接口,就一定要传 name 和 age 属性

使用 Partial 改造一下,就可以变成可选属性,

interface IPerson {
    name: string
    age: number
}

type IPartial = Partial<IPerson>

let p1: IPartial = {}
1
2
3
4
5
6
7
8

Partial 原理

Partial 的实现用到了 `in``keyof`

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P]
}
1
2
3
4
5
6
7
8
  • [P in keyof T]遍历 T 上的所有属性
  • ?:设置为属性为可选的
  • T[P]设置类型为原来的类型

# Readonly

Readonly<T>将 T 的所有属性映射为只读的,例如:

interface IPerson {
  name: string
  age: number
}

type IReadOnly = Readonly<IPerson>

let p1: IReadOnly = {
  name: 'lin',
  age: 18
}
1
2
3
4
5
6
7
8
9
10
11

Readonly 原理,和 Partial 几乎完全一样,

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}
1
2
3
4
5
6
  • [P in keyof T]遍历 T 上的所有属性
  • readonly设置为属性为可选的
  • T[P]设置类型为原来的类型

# Pick

Pick 用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:

interface IPerson {
  name: string
  age: number
  sex: string
}

type IPick = Pick<IPerson, 'name' | 'age'>


let p1: IPick = {
  name: 'lin',
  age: 18
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Pick 原理

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}
1
2
3
4
5
6

Pick 映射类型有两个参数:

  • 第一个参数 T,表示要抽取的目标对象
  • 第二个参数 K,具有一个约束:K 一定要来自 T 所有属性字面量的联合类型

# Record

上面三种映射类型官方称为同态,意思是只作用于 obj 属性而不会引入新的属性。

Record 是会创建新属性的非同态映射类型。

interface IPerson {
  name: string
  age: number
}

type IRecord = Record<string, IPerson>

let personMap: IRecord = {
   person1: {
       name: 'lin',
       age: 18
   },
   person2: {
       name: 'liu',
       age: 25
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Record 原理

/**
 * 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
  • 第一个参数可以传入继承于 any 的任何值
  • 第二个参数,作为新创建对象的值,被传入。

# 条件类型

T extends U ? X : Y
1

Exclude 和 Extract 的实现就用到了条件类型。

# Exclude

Exclude 意思是不包含,Exclude<T, U> 会返回 联合类型 T 中不包含 联合类型 U 的部分。

// b | c
type Test = Exclude<"a" | "b" | "c", "a">;
1
2

Exclude 原理

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T
1
2
3
4
  • never 表示一个不存在的类型
  • never 与其他类型的联合后,为其他类型
// string | number
type Test = string | number | never;
1
2

# Extract

Extract<T, U>提取联合类型 T 和联合类型 U 的所有交集。

type Test = Extract<"key1" | "key2", "key1">;
1

Extract 原理

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never
1
2
3
4

# 工具类型(Utility Types)

上文介绍的索引类型、映射类型和条件类型都是工具类型

# Omit

Omit<T, U>从类型 T 中剔除 U 中的所有属性。

interface IPerson {
    name: string
    age: number
}

type IOmit = Omit<IPerson, 'age'>
1
2
3
4
5
6

这样就剔除了 IPerson 上的 age 属性。

Omit 原理

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> =
Pick<T, Exclude<keyof T, K>>
1
2
3
4
5

Pick 用于挑选一组属性并组成一个新的类型,Omit 是剔除一些属性,留下剩余的,他们俩有点相反的感觉。

那么就可以用 Pick 和 Exclude 实现 Omit。

当然也可以不用 Pick 实现,

type Omit2<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}
1
2
3

# NonNullable

NonNullable<T> 用来过滤类型中的 nullundefined 类型。

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
1
2

NonNullable 原理

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> =
T extends null | undefined ? never : T
1
2
3
4
5
  • never 表示一个不存在的类型
  • never 与其他类型的联合后,为其他类型

# Parameters

Parameters 获取函数的参数类型,将每个参数类型放在一个元组中。

type T1 = Parameters<() => string>; // []

type T2 = Parameters<(arg: string) => void>; // [string]

type T3 = Parameters<(arg1: string, arg2: number) => void>; // [arg1: string, arg2: number]
1
2
3
4
5

Parameters 原理

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never
1
2
3
4
5

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

  • Parameters 首先约束参数 T 必须是个函数类型
  • 判断 T 是否是函数类型,如果是则使用 infer P 暂时存一下函数的参数类型,后面的语句直接用 P 即可得到这个类型并返回,否则就返回 never

# ReturnType

ReturnType 获取函数的返回值类型。

type T0 = ReturnType<() => string>; // string

type T1 = ReturnType<(s: string) => void>; // void
1
2
3

ReturnType 原理

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any
1
2
3
4
5

懂了 Parameters,也就懂了 ReturnType,

  • ReturnType 首先约束参数 T 必须是个函数类型
  • 判断 T 是否是函数类型,如果是则使用 infer R 暂时存一下函数的返回值类型,后面的语句直接用 R 即可得到这个类型并返回,否则就返回 any

# TS 声明文件

# declare

当使用第三方库时,很多三方库不是用 TS 写的,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

比如,在 TS 中直接使用 Vue,就会报错

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
1
2
3
4
5
6

# .d.ts

通常我们会把声明语句放到一个单独的文件(Vue.d.ts)中,这就是声明文件,以 .d.ts 为后缀。

# 使用三方库

那么当我们使用三方库的时候,是不是所有的三方库都要写一大堆 decare 的文件呢?

答案是不一定,要看社区里有没有这个三方库的 TS 类型包(一般都有)。

社区使用 @types 统一管理第三方库的声明文件,是由 DefinitelyTyped[11] 这个组织统一管理的

比如安装 lodash 的类型包,

# 自己写声明文件

比如你以前写了一个请求小模块 myFetch

function myFetch(url, method, data) {
    return fetch(url, {
        body: data ? JSON.stringify(data) : '',
        method
    }).then(res => res.json())
}

myFetch.get = (url) => {
    return myFetch(url, 'GET')
}

myFetch.post = (url, data) => {
    return myFetch(url, 'POST', data)
}

export default myFetch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

现在新项目用了 TS 了,要在新项目中继续用这个 myFetch,你有两种选择:

  • 用 TS 重写 myFetch,新项目引重写的 myFetch
  • 直接引 myFetch ,给它写声明文件

选择第二种方案

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

declare function myFetch<T = any>(url: string, method: HTTPMethod, data?: any): Promise<T>

declare namespace myFetch { // 使用 namespace 来声明对象下的属性和方法
    const get: <T = any>(url: string) => Promise<T> 
    const post: <T = any>(url: string, data: any) => Promise<T>
}
1
2
3
4
5
6
7
8

比较麻烦的是需要配置才行,可以有两种选择,

  • 创建一个 node_modules/@types/myFetch/index.d.ts 文件,存放 myFetch 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。
  • 创建一个 types 目录,专门用来管理自己写的声明文件,将 myFetch 的声明文件放到 types/