Featured image of post TypeScript进阶

TypeScript进阶

类与泛型的使用

类(Class)

ES6 中:基于原型继承的语法糖

JavaScript是一门基于原型的语言,通过原型继承,我们可以实现类的一些性质

原型与原型链

如何实现方法和属性的继承?

错误的方法 ❎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Student(grade, name, age) {
  this.name = name;
  this.age = age;
  this.grade = grade;
}

Person.prototype.sayHello = function () {
  console.log(`Hello, my name is ${this.name}`);
};

🚫 声明冗余,并且不会继承Person的原型方法

正确方法 ✅

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Student(grade, name, age) {
  Person.call(this, name, age);
  this.grade = grade;
}

Person.prototype.sayHello = function () {
  console.log(`Hello, my name is ${this.name}`);
};

// 子类继承父类
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Person;

⚠️ 需要让Student的原型也继承Person的原型,并让Student原型对象的构造函数也指向Person,从而完整构建出一条原型链

使用类(Class)的语法糖会让代码更具可读性和可维护性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

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

上述两种代码的效果是一样的

静态属性(方法)

默认情况下,在类中不加任何关键字声明方法的都是公共方法,会定义在类的原型对象上,被其实例链式继承

使用static关键字声明一个静态域

和公共域的区别:通过类本身访问和类实例访问

私有属性(方法)

有时候,我们不希望类的属性(方法)暴露给外部使用,这些属性(方法)往往仅仅会被使用在类的内部,此时我们可以通过声明类私有域来进行对属性(方法)的私有化

ES2019中增加了对class私有属性的原生支持

使用#(哈希前缀)声明一个私有属性(方法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  #_combineFullName() {
    return this.firstName + " " + this.lastName;
  }

  getName() {
    return this.#_combineFullName();
  }
}

Q:不使用哈希前缀如何声明私有属性(方法)?

A:命名规定(自欺欺人法);使用闭包;将私有方法定义在类的外部;使用WeakMap

TypeScript 中:完备的类实现

我们可以直接使用publicstaticprivate来区别声明公共域、静态域和私有域

并且可以声明抽象类、使用readonly修饰符以及装饰器等,更好地实现面向对象

示例:

 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
class Person {
  // 需要先声明示例属性的类型
  firstName: string;
  lastName: string;
  age: number;
  static readonly description: string = "Person";

  constructor(firstName: string, lastName: string, age: number) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }

  private _combineFullName(): string {
    return this.firstName + " " + this.lastName;
  }

  public getFullName(): string {
    return this._combineFullName(); // 只能在类的内部访问到这个方法
  }

  static getDescription(): string {
    return this.description; // 任何试图修改只读属性的操作都会报错
  }

  static getAge(person: Person): number {
    return person.age;
  }
}

class Student extends Person {
  grade: number;
  id: string;
  private _info: string = "default info";

  constructor(
    firstName: string,
    lastName: string,
    age: number,
    grade: number,
    id: string
  ) {
    super(firstName, lastName, age);
    this.grade = grade;
    this.id = id;
  }

  get info(): string {
    return this._info;
  }

  set info(value: string) {
    this._info = value;
  }
}

const John = new Student("John", "Doe", 30, 9, "12345678");

/* 正确的操作 */
John.getFullName(); // "John Doe"
Person.getDescription(); // "Person"
Person.getAge(John); // 30

// 只能通过 `getter` 和 `setter` 操作私有属性
John.info = "new info";
console.log(John.info); // "new info"

/**
 * John.getAge()
 * 报错,因为getAge方法是静态方法,不能在类的实例上调用
 */

/**
 * John.combineFullName()
 * Person.combineFullName()
 * 报错,因为combineFullName方法是私有方法,不能在类的外部调用
 */

泛型(generics)

泛型函数(方法)

1
2
3
4
5
6
7
8
function identity<T, U>(value: T, message: U): T {
  console.log(message);
  return value;
}
const res = identity<number, string>(21, "Hello");
console.log(res);
// Hello
// 21

在多数场景下,我们调用函数时并不需要手动将类型写出来,ts 的类型系统会自动进行类型的推断,并且不会报错。只有在 ts 无法推断类型时,我们才需要显式声明泛型变量的类型。

1
2
3
4
5
6
7
function identity<T, U>(value: T, message: U): T {
  console.log(message);
  return value;
}
// 写成这样也是完全可以的
const res = identity(21, "Hello");
console.log(res);

使用泛型可以防止类型的丢失

1
2
3
4
5
6
7
8
9
function identity(value: number | string, message: any) {
  console.log(message);
  return value;
}

const res = identity(21, "Hello");
// 报错:不能将类型 `string | number` 分配给类型 `number`
// 在这个过程中类型并没有收窄
const value: number = res;

既然使用了 ts,就要尽量避免使用 any,因为这样类型系统就没有了意义

1
2
3
4
5
6
7
function identity(value: any, message: any) {
  console.log(message);
  return value;
}
const res = identity(21, "Hello");
// 合法,ts无法判断res的类型,
const value: string = res;

泛型接口

 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 IResult<T> {
  code: number;
  message: string;
  data: T;
  cache: boolean;
}

interface IUserInfo {
  name: string;
  age: number;
  type: number;
}

// 类型别名
type IUserInfoResponse = IResult<IUserInfo>;
/* 等同于 {
  code: number;
  message: string;
  data: {
    name: string;
    age: number;
    type: number;
  };
  cache: boolean;
} */

泛型接口和泛型函数在定义时都可以指定缺省值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface IResult<T = IUserInfo> {
  code: number;
  message: string;
  data: T;
  cache: boolean;
}

interface IUserInfo {
  name: string;
  age: number;
  type: number;
}

在泛型函数定义中指定缺省值没有什么实际意义,因为 ts 本身就会根据传参来确定参数类型

泛型继承

和类一样,接口也可以进行类型的继承:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface IBaseType {
  className?: string;
  style?: string;
  children?: ReactElement | ReactElement[];
}

interface IButtonProps extends IBaseType {
  onClick?: () => void;
}

/**  等同于
 * interface IButtonProps {
 *   className?: string;
 *   style?: string;
 *   children?: ReactElement | ReactElement[];
 *   onClick?: () => void;
 * }
 */

热知识:TypeScript 的类型系统是图灵完备的

拓展阅读:探究 typescript 类型元编程

而泛型变量也可以进行继承:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface IPerson {
  name: string;
  age: number;
}

function sayHi<T extends IPerson>(o: T): void {
  console.log("Hi, I'm " + o.name);
  // 只能提示 `name` 和 `age` 两个成员
}

const obj = {
  name: "Jack",
  age: 20,
  info: "info",
};

sayHi(obj); // 即使 `obj` 上多了 `info` 成员也是合法的

泛型变量继承了IPerson接口后,就可以对传参obj进行类型约束和成员提示了

如果已经声明了一个对象,也可以通过typeof关键字直接获取对象的类型:

1
2
3
4
5
6
7
8
const obj = {
  name: "Jack",
  age: 20,
};

function sayHi<T extends typeof obj>(o: T): void {
  console.log("Hi, I'm " + o.name);
}

泛型关键字

  1. extends:继承

  2. typeof:获取变量的类型

  3. keyof:获取一个对象接口的所有 key 值

1
2
3
4
5
6
7
interface IPerson {
  name: string;
  age: number;
  gender: number;
}

type K = keyof IPerson; // "name" | "age" | "gender"

使用keyof实现pluck函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const obj = {
  name: "Jack",
  age: 20,
  info: "info",
};

function pluck<T, K extends keyof T>(obj: T, names: K[]): Array<T[K]> {
  return names.map(name => obj[name]);
}

pluck(obj, ["name", "age"]); // ['Jack', 20]
  1. in:遍历枚举类型
1
2
3
4
type Keys = "a" | "b";
type Obj = {
  [p in Keys]: any;
}; //  { a: any, b: any }
  1. infer:推断出最能够匹配extends左边接受的类型(最接近的类型)
1
2
3
type Flatten<T> = T extends Array<infer R> ? R : T;
type strArr = Flatten<Array<string>>; // string 类型
type numArr = Flatten<Array<"123">>; // '123' 类型

工具泛型

keyof可以获取类型中的所有键名,而in可以对枚举类型进行遍历,因此两者联合使用就可以遍历所有键名了

  1. Partial:将传入的属性变为可选项
1
2
3
type Partial<T> = {
  [P in keyof T]?: T[P];
};

其中,keyof T拿到T中的所有键名,然后in进行遍历,将值赋给P,最后T[P]取得相应属性值。

  1. Required:将传入的属性变为必选项
1
2
3
type Required<T> = {
  [P in keyof T]-?: T[P];
};

其中,-?表示将可选项的?去掉,将其改为+?则可以将所有属性改为可选项

  1. Readonly:将传入的属性变为只读选项
1
2
3
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

此外还有RecordPickExclude等诸多工具泛型,这些都是官方帮我们封装好的,可以开箱即用。

类型断言

有时候你比编译器更了解某个变量的类型,因此 ts 给你提供了覆盖其推断的能力,即断言。在初始化数据的时候,我们通常会使用类型断言。

1
2
3
4
5
6
7
interface ICourseInfo {
  courseName: string;
  courseId: number;
  location: string;
}

const courseInfo = {} as ICourseInfo;

Vue3中,Props的类型约束就依赖类型断言实现

 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
import { defineComponent, PropType } from "vue";

interface Book {
  title: string;
  author: string;
  year: number;
}

const Component = defineComponent({
  props: {
    name: String,
    id: [Number, String],
    success: { type: String },
    callback: {
      type: Function as PropType<() => void>,
    },
    book: {
      type: Object as PropType<Book>,
      required: true,
    },
    metadata: {
      type: null, // metadata 的类型是 any
    },
  },
});

在下面的例子中,原始数据在经过groupBy后丢失了类型,因此在下面我们需要使用类型断言,将dayCourses强制认定为ICourseDetails[],这样才能够对其调用forEach方法,并且循环中的course也自动推断出了ICourseDetails的类型,并且会进行提示。

这就使得开发者在繁杂的逻辑中,可以从 IDE 层面规避不必要的 bug,这也是 ts 诞生的一个重要原因

1
2
3
4
5
6
7
8
const groupByWeekDay = groupBy(courses, "WeekDay");
// 对每一天进行遍历
for (let [day, dayCourses] of Object.entries(groupByWeekDay)) {
  // 用于存放每一天分组后的课程
  const groupBySection = [] as ICourseDetails[][];
  // 取出 `dayCourses` 中的每一节课,将其与已分组的 `group` 进行判断
  (dayCourses as ICourseDetails[]).forEach((course, index) => {});
}

可选链与空值合并

在写 js 的时候,访问对象属性是一件有风险的事情,有时我们并不能确定我们一定可以访问到这个值,不然的话就会报熟悉的Cannot Read Property of Undefined

在大多数情况下我们可能会使用:

1
const result = obj && obj.name;

来解决,但当数据层级很深的情况下,我们就需要做一层一层的分支判断了:

1
store.user.product.name;
1
2
3
console.log(
  store && store.user && store.user.product && store.user.product.name
);

这样的代码逻辑冗余而且十分丑陋,好在我们现在可以使用可选链操作符来简化操作,只需要在.前加一个?即可

1
store?.user?.product?.name;

这样一来,如果中间的链条在任何一处断开,都不会报错,而只会短路返回一个undefined,那么结合空值合并运算符就可以写出这样的代码了

1
store?.user?.product?.name ?? "default name";

这里的??表示,当左边的表达式为null或者undefined时,整个式子取右边的值,比常规的||要更加精确。

可选链支持访问对象的所有属性,这意味着你可以对数组和方法都写出这样的判断

1
2
props?.products?.[1];
props?.getName?.();

题外话:在一次 pr 中,尤雨溪回复了为什么不在框架源码中使用可选链操作符,原因是在 ts 中使用可选链会被编译成更复杂的语句。

在大型框架的设计中,打包体积是不得不考虑的一个问题,但在我们的日常开发中,放心用就行啦。

非空断言

有时,我们可以确定某个对象的属性一定不为空,例如我们常常会在Axios的拦截器中设置headers,ts 往往会提示,对象可能未定义,但事实上我们知道它一定已经定义了,所以我们可以在其后加上!来断言非空,从而给它赋值

1
2
3
4
Axios.interceptors.request.use((config: AxiosRequestConfig) => {
  config.headers!.Authorization = `Bearer ${localStorage.token}`;
  return config;
});

但如果要贯彻函数式编程的思想,这样写也许会更优雅:

1
2
3
4
5
6
7
Axios.interceptors.request.use((config: AxiosRequestConfig) => {
  config.headers = {
    ...config.headers,
    Authorization: `Bearer ${localStorage.token}`,
  };
  return config;
});
Built with Hugo
主题 StackJimmy 设计