# 什么是 TypeScript

  • TypeScript (opens new window) 是添加了类型系统的 JavaScript,适用于任何规模的项目。

  • TypeScript 是一门静态类型、弱类型的语言。

  • TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。

  • TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。

  • TypeScript 拥有很多编译选项,类型检查的严格程度由你决定。

  • TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript。

  • TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。

  • TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明。

  • TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)。

# TypeScript 的优点

  • 规避⼤量低级错误,避免时间浪费,省时。

  • 减少多⼈协作项⽬的成本,⼤型项⽬友好,省⼒。

  • 良好代码提⽰,不⽤反复⽂件跳转或者翻⽂档,省⼼。

# TypeScript 的缺点

  • 与实际框架结合会有很多坑。

  • 配置学习成本⾼。

  • TypeScript 的类型系统其实⽐较复杂。

# 基本类型

  • 布尔类型:boolean

  • 数字类型:number

  • 字符串类型:string

  • 空值:void

  • Null 和 Undefined:nullundefined

  • Symbol 类型:symbol

  • BigInt ⼤数整数类型:bigint

# 构造函数和类型的区别

很多 TypeScript 的原始类型⽐如 boolean、number、string 等等,在 JavaScript 中都有类似的关键字 Boolean、Number、String,后者是 JavaScript 的构造函数。

⽐如我们⽤ Number ⽤于数字类型转化或者构造 Number 对象⽤的,⽽ TypeScript 中的 number 类型仅仅是表⽰类型,两者完全不同。

再比如使用构造函数 Boolean 创造的对象不是布尔值:

let createdByNewBoolean: boolean = new Boolean(1);

// Type 'Boolean' is not assignable to type 'boolean'.
// 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
1
2
3
4

事实上 new Boolean() 返回的是一个 Boolean 对象:

let createdByNewBoolean: Boolean = new Boolean(1);
1

直接调用 Boolean 也可以返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);
1

# void 和 null、undefined 的区别

与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

// 这样不会报错
let num: number = undefined;

// 这样也不会报错
let u: undefined;
let num: number = u;
1
2
3
4
5
6

而 void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.
1
2
3
4

# any 和 unknown 的区别

any 类型是多⼈协作项⽬的⼤忌,很可能把 Typescript 变成 AnyScript,通常不在迫不得已的情况下,不应该⾸先考虑使⽤此类型。

unknown 是 TypeScript 3.0 引⼊的新类型,是 any 类型对应的安全类型。

let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
1
2
3
4
5
6
7
8
9
10
11

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
1
2
3
4
5
6
7
8
9

虽然它们都可以是任何类型,但是在 unknown 类型被确定是某个类型之前,它不能被进⾏任何操作⽐如实例化、getter、函数执⾏等等。

let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
1
2
3
4
5
6

# object、Object 和 {} 类型

# object 类型

object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  create(o: object | null): any;
  // ...
}

const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
1
2
3
4
5
6
7
8
9
10
11
12
13

# Object 类型

Object 类型:它是所有 Object 类的实例的类型,它由以下两个接口来定义:

  • Object 接口定义了 Object.prototype 原型对象上的属性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}
1
2
3
4
5
6
7
8
9
10
  • ObjectConstructor 接口定义了 Object 类的属性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  /** Invocation via `new` */
  new (value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}
declare var Object: ObjectConstructor;
1
2
3
4
5
6
7
8
9
10
11

Object 类的所有实例都继承了 Object 接口中的所有属性。

# {} 类型

{} 类型描述了一个没有成员的对象。当我们试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误。

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
1
2
3
4
5

但是,我们仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用:

// Type {}
const obj = {};

// "[object Object]"
obj.toString();
1
2
3
4
5

# never 类型

never 类型表⽰的是那些永不存在的值的类型。例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

never 类型是任何类型的⼦类型,也可以赋值给任何类型;然⽽,没有类型是 never 的⼦类型或可以赋值给 never 类型(除了 never 本⾝之外)。

// 抛出异常的函数永远不会有返回值
function error(message: string): never {
  throw new Error(message);
}

// 根本就不会有返回值的函数
function infiniteLoop(): never {
  while (true) {}
}

// 空数组,⽽且永远是空的
const empty: never[] = [];
1
2
3
4
5
6
7
8
9
10
11
12

# 利用 never 类型进行全面性检查

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,比如:

type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}
1
2
3
4
5
6
7
8
9
10
11

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean;
1

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。

因此,我们可以使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

# 访问联合类型的属性或方法

当 TypeScript 不确定一个联合类型 (opens new window)的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number): number {
  return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
1
2
3
4
5
6

上例中,length 不是 string 和 number 的共有属性,所以会报错。

访问 string 和 number 的共有属性是没问题的:

function getString(something: string | number): string {
  return something.toString();
}
1
2
3

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber: string | number;
myFavoriteNumber = "seven";
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.
1
2
3
4
5
6
7

上例中,第二行的 myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错。

而第四行的 myFavoriteNumber 被推断成了 number,访问它的 length 属性时就报错了。

# 可辨识联合类型

可辨识联合(Discriminated Unions)类型,也称为代数数据类型标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫

这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

# 可辨识

可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:

enum CarTransmission {
  Automatic = 200,
  Manual = 300
}
interface Motorcycle {
  vType: "motorcycle"; // discriminant
  make: number; // year
}
interface Car {
  vType: "car"; // discriminant
  transmission: CarTransmission;
}
interface Truck {
  vType: "truck"; // discriminant
  capacity: number; // in tons
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在上述代码中,我们分别定义了 Motorcycle 、 Car 和 Truck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。

# 联合类型

基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:

type Vehicle = Motorcycle | Car | Truck;
1

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的⻋辆。

# 类型守卫

下面我们来定义一个 evaluatePrice 方法,该方法用于根据⻋辆的类型、容量和评估因子来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI;

function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
1
2
3
4
5
6
7
8

对于以上代码,TypeScript 编译器将会提示以下错误信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
1
2

原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
  switch (vehicle.vType) {
    case "car":
      return vehicle.transmission * EVALUATION_FACTOR;
    case "truck":
      return vehicle.capacity * EVALUATION_FACTOR;
    case "motorcycle":
      return vehicle.make * EVALUATION_FACTOR;
  }
}
1
2
3
4
5
6
7
8
9
10

在以上代码中,我们使用 switch 和 case 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该⻋辆类型所对应的价格。

# 其他例子

再来看一个例子。

我们先假设⼀个场景,现在有两个功能,⼀个是创建⽤户即 create ,⼀个是删除⽤户即 delete。

我们先定义⼀下这个接⼝,由于创建⽤户不需要 id,是系统随机⽣成的,⽽删除⽤户是必须⽤到 id 的,那么代码如下:

interface Info {
  username: string;
}

interface UserAction {
  id?: number;
  action: "create" | "delete";
  info: Info;
}
1
2
3
4
5
6
7
8
9

上⾯的接⼝是不是有什么问题?

是的,当我们创建⽤户时是不需要 id 的,但是根据上⾯接⼝产⽣的情况,以下代码是合法的:

const action: UserAction = {
  action: "create",
  id: 111,
  info: {
    username: "xiaomuzhu"
  }
};
1
2
3
4
5
6
7

但是我们明明不需要 id 这个字段,因此我们得⽤另外的⽅法,这就⽤到了上⾯提到的「类型字⾯量」了:

type UserAction =
  | {
      id: number;
      action: "delete";
      info: Info;
    }
  | {
      action: "create";
      info: Info;
    };
1
2
3
4
5
6
7
8
9
10
const UserReducer = (userAction: UserAction) => {
  switch (userAction.action) {
    case "delete":
      console.log(userAction.id);
      break;
    default:
      break;
  }
};
1
2
3
4
5
6
7
8
9

我们上面提到了 userAction.action 就是辨识的关键,被称为可辨识的标签。我们发现上面这种模式要想实现必须要三个要素:

  • 具有普通的单例类型属性——可辨识的特征,上文中就是 delete 与 create 两个有唯一性的字符串字面量。

  • 一个类型别名包含联合类型。

  • 类型守卫的特性,比如我们必须用 if switch 来判断 userAction.action 是属于哪个类型作用域即 delete 与 create。

# 类型别名

类型别名常用于联合类型。使用 type 创建类型别名。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}

type some = boolean | string;
const b: some = true; // ok
const c: some = "hello"; // ok
const d: some = 123; // 不能将类型“123”分配给类型“some”

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 接口定义可选、只读和任意属性

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象 (opens new window)以外,也常用于对「对象的形状(Shape)」进行描述 (opens new window)

interface Person {
  readonly id: number;
  name: string;
  age?: number;
  [propName: string]: any;
}
1
2
3
4
5
6

一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。

interface Person {
  name: string;
  age?: number;
  [propName: string]: string;
}

let tom: Person = {
  name: "Tom",
  age: 25,
  gender: "male"
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
  name: string;
  age?: number;
  [propName: string]: string | number;
}

let tom: Person = {
  name: "Tom",
  age: 25,
  gender: "male"
};
1
2
3
4
5
6
7
8
9
10
11

# 只读数组类型

TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
1
2
3
4
5
6
7

# interface 和 type 的区别

官方推荐可能的情况下更多的使用 interface。

# 描述对象和函数

interface 和 type 都可以用来描述对象的形状或函数签名。

// interface
interface Point {
  x: number;
  y: number;
}
interface SetPoint {
  (x: number, y: number): void;
}

// type
type Point = { x: number; y: number };
type SetPoint = (x: number, y: number) => void;
1
2
3
4
5
6
7
8
9
10
11
12

# 描述其他类型

与 interface 不同的是,type 还可以用于定义一些其他类型,比如原始类型、联合类型和元组,适⽤范围显然更⼴

// primitive
type Name = string;

// object
type PartialPointX = { x: number };
type PartialPointY = { y: number };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];
1
2
3
4
5
6
7
8
9
10
11
12

# 扩展

interface 和 type 都能够被扩展,但语法有所不同。此外,interface 和 type 不是互斥的。interface 可以扩展 type,反之亦然。

// Interface extends interface
interface PartialPointX {
  x: number;
}
interface Point extends PartialPointX {
  y: number;
}

// Type alias extends type alias
type PartialPointX = { x: number };
type Point = PartialPointX & { y: number };

// Interface extends type alias
type PartialPointX = { x: number };
interface Point extends PartialPointX {
  y: number;
}

// Type alias extends interface
interface PartialPointX {
  x: number;
}
type Point = PartialPointX & { y: number };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// normal work
interface NumLogger {
  log: (val: number) => void;
}
type StrAndNumLogger = NumLogger & {
  log: (val: string) => void;
};
const logger: StrAndNumLogger = {
  log: (val: string | number) => console.log(val)
};
logger.log(1);
logger.log("hi");

// error:Interface 'StrAndNumLogger' incorrectly extends interface 'NumLogger'
interface NumLogger {
  log: (val: number) => void;
}
interface StrAndNumLogger extends NumLogger {
  log: (val: string) => void;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 实现

类可以以相同的方式实现 interface 或 type,但类不能实现使用 type 定义的联合类型

interface Point {
  x: number;
  y: number;
}
class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = { x: number; y: number };
class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number } | { y: number };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint {
  // Error
  x = 1;
  y = 2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 合并

与 type 不同的是,interface 可以定义多次,会被自动合并为单个接口

interface Point {
  x: number;
}
interface Point {
  y: number;
}
const point: Point = { x: 1, y: 2 };
1
2
3
4
5
6
7

# 何时使用

在以下情况使用 type:

// 范型转换:在将多个类型转换为单个泛型类型时使用
type Nullable<T> = T | null | undefined;
type NonNull<T> = T extends null | undefined ? never : T;

// 类型别名:可以使用 type 为难以阅读以及不便重复使用的长或复杂类型创建别名
type Primitive = number | string | boolean | null | undefined;

// 类型捕获:当类型未知时,使用 type 来捕获对象的类型
const orange = { color: "Orange", vitamin: "C" };
type Fruit = typeof orange;
let apple: Fruit;
1
2
3
4
5
6
7
8
9
10
11

在以下情况使用 interface:

// 多态性
interface Bird {
  size: number
  fly(): void
  sleep(): void
}

class Hummingbird implements Bird { ... }
class Bellbird implements Bird { ... }

// 声明合并:下面两个同名的接口会自动合并成一个
interface Bird {
  size: number
  fly(): void
  sleep(): void
}
interface Bird {
  color: string
  eat(): void
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 用接口表示数组

接口也可以用来描述数组:

interface NumberArray {
  [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
1
2
3
4

NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。

虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比类型+方括号数组泛型这两种方式复杂多了。

不过有一种情况例外,那就是它常用来表示类数组。

类数组(Array-like Object)不是数组类型,比如 arguments:

function sum() {
  let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
1
2
3
4
5

本例中,arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:

function sum() {
  let args: {
    [index: number]: number;
    length: number;
    callee: Function;
  } = arguments;
}
1
2
3
4
5
6
7

在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length 和 callee 两个属性。

事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
  let args: IArguments = arguments;
}
1
2
3

其中 IArguments 是 TypeScript 中定义好了的类型,即内置对象 (opens new window),它实际上就是:

interface IArguments {
  [index: number]: any;
  length: number;
  callee: Function;
}
1
2
3
4
5

# 函数表达式的 ts 定义

如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:

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

这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样:

let mySum: (x: number, y: number) => number = function(
  x: number,
  y: number
): number {
  return x + y;
};
1
2
3
4
5
6

# 用接口定义函数的形状

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  return source.search(subString) !== -1;
};
1
2
3
4
5
6
7
8

# 函数的可选参数、默认参数和剩余参数

函数的可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:

function buildName(firstName?: string, lastName: string) {
  if (firstName) {
    return firstName + " " + lastName;
  } else {
    return lastName;
  }
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName(undefined, "Tom");

// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.
1
2
3
4
5
6
7
8
9
10
11

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = "Cat") {
  return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");
1
2
3
4
5

此时就不受「可选参数必须接在必需参数后面」的限制了:

function buildName(firstName: string = "Tom", lastName: string) {
  return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let cat = buildName(undefined, "Cat");
1
2
3
4
5

在使用剩余参数时,也只能是最后一个参数。

function push(array: any[], ...items: any[]) {
  items.forEach(function(item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);
1
2
3
4
5
6
7
8

# 函数重载

重载允许一个函数在接受不同数量或类型的参数时,作出不同的处理。

在 TypeScript 中,我们可以使用函数重载(Function Overloads)来实现根据不同参数数量返回不同类型的函数。

// 重载定义
function getResult(): string;
function getResult(param1: number): number;
function getResult(param1: string, param2: boolean): boolean;

// 实现
function getResult(...args: any[]): string | number | boolean {
  if (args.length === 0) {
    return "default";
  } else if (args.length === 1 && typeof args[0] === "number") {
    return args[0] * 2;
  } else if (
    args.length === 2 &&
    typeof args[0] === "string" &&
    typeof args[1] === "boolean"
  ) {
    return args[1];
  }

  throw new Error("Invalid arguments");
}

// 使用
const result1: string = getResult(); // 返回类型为 string
const result2: number = getResult(5); // 返回类型为 number
const result3: boolean = getResult("hello", true); // 返回类型为 boolean
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
function reverse(x: number): number;
function reverse(x: string): string;

function reverse(x: number | string): number | string {
  if (typeof x === "number") {
    return Number(
      x
        .toString()
        .split("")
        .reverse()
        .join("")
    );
  } else if (typeof x === "string") {
    return x
      .split("")
      .reverse()
      .join("");
  }
  throw new Error("Invalid arguments");
}

const a = reverse(123); // number
const b = reverse("123"); // string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在这个例子中,我们重复定义了多次函数 reverse,前两次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

# 类型断言

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement; // as 语法
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas"); // 尖括号语法
1
2
// 有时候可以先将类型转换成 any 或者 unknown,然后再转换成最终的类型,以达到强制类型转换
const a = ((expr as any) / unknown) as T;
1
2

注意

因为类型断言在编译时会被移除,所以没有与类型断言关联的运行时检查。如果类型断言错误,则不会产生异常或 null。

# 文字类型

Literal Types (opens new window)

// 字符串文字类型
function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre"); // error

// 数字文字类型
function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

// 文字类型与非文字类型混用
interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic"); // error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 文字推理
const req = { url: "https://example.com", method: "GET" };
// 这里 req.method 的类型会被推断为字符串而不是 "GET",所以 typescript 认为有错误
handleRequest(req.url, req.method); // error: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

// 解决方法有两种

// 方法一
const req = { url: "https://example.com", method: "GET" as "GET" }; // 打算让 req.method 始终具有文字类型 "GET"
// 或者
handleRequest(req.url, req.method as "GET"); // 我知道 req.method 的值为 "GET"

// 方法二,使用 as const 将整个对象转换为类型文字
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 非空断言操作符(!)

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

非空断言操作符的一些使用场景如下:

# 忽略 undefined 和 null 类型

function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'.
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
1
2
3
4
5
6

# 调用函数时忽略 undefined 类型

type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}
1
2
3
4
5
6
7

因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中要特别注意。比如:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
1
2
3

以上 TS 代码会编译生成以下 ES5 代码:

"use strict";
const a = undefined;
const b = a;
console.log(b);
1
2
3
4

虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined 。

# 确定赋值断言

在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。比如:

let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error

function initialize() {
  x = 10;
}
1
2
3
4
5
6
7
8
9

很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:

let x!: number;
initialize();

console.log(2 * x); // Ok

function initialize() {
  x = 10;
}
1
2
3
4
5
6
7
8

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。

# 空值合并运算符(??)

当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。

与逻辑或 || 运算符不同,逻辑或会在左操作数为 false 值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认的值时,可能会遇到意料之外的行为。比如为 false 值(''、NaN 或 0)时。

const foo = null ?? "default string";
console.log(foo); // 输出: "default string"

const baz = 0 ?? 42;
console.log(baz); // 输出: 0
1
2
3
4
5

以上 TS 代码经过编译后,会生成以下 ES5 代码:

"use strict";
var _a, _b;
var foo = (_a = null) !== null && _a !== void 0 ? _a : "default string";
console.log(foo); // 输出: "default string"

var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz); // 输出: 0
1
2
3
4
5
6
7

# 短路

当空值合并运算符的左表达式不为 null 或 undefined 时,不会对右表达式进行求值。

function A() {
  console.log("A was called");
  return undefined;
}
function B() {
  console.log("B was called");
  return false;
}
function C() {
  console.log("C was called");
  return "foo";
}
console.log(A() ?? C());
console.log(B() ?? C());
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以上代码执行结果如下:

A was called
C was called
foo
B was called
false
1
2
3
4
5

# 不能与 && 或 || 操作符共用

若空值合并运算符 ?? 直接与 AND(&&) 和 OR(||) 操作符组合使用会抛出 SyntaxError 的错误。

// '||' and '??' operations cannot be mixed without parentheses.(5076)
(null || undefined) ?? "foo"; // raises a SyntaxError

// '&&' and '??' operations cannot be mixed without parentheses.(5076)
(true && undefined) ?? "foo"; // raises a SyntaxError
1
2
3
4
5

但当使用括号来显式表明优先级时是可行的,比如:

(null || undefined) ?? "foo"; // 返回 "foo"
1

# 与可选链操作符 ?. 的关系

空值合并运算符针对 undefined 与 null 这两个值,可选链式操作符 ?. 也是如此。可选链式操作符,对于访问属性可能为 undefined 与 null 的对象时非常有用。

interface Customer {
  name: string;
  city?: string;
}

let customer: Customer = { name: "Semlinker" };

let customerCity = customer?.city ?? "Unknown city";
console.log(customerCity); // 输出: Unknown city
1
2
3
4
5
6
7
8
9

空值合并运算符不仅可以在 TypeScript 3.7 以上版本中使用。当然也可以在 JavaScript 的环境中使用它,但需要借助 Babel,在 Babel 7.8.0 版本也开始支持空值合并运算符。

# 数字分隔符(_)

TypeScript 2.7 带来了对数字分隔符的支持,正如数值分隔符 ECMAScript 提案中所概述的那样。对于一个数字字面量,你现在可以通过把一个下划线作为它们之间的分隔符来分组数字:

const inhabitantsOfMunich = 1_464_301;
const distanceEarthSunInKm = 149_600_000;
const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;
1
2
3
4

分隔符不会改变数值字面量的值,但逻辑分组使人们更容易一眼就能读懂数字。以上 TS 代码经过编译后,会生成以下 ES5 代码:

"use strict";
var inhabitantsOfMunich = 1464301;
var distanceEarthSunInKm = 149600000;
var fileSystemPermission = 504;
var bytes = 262926349;
1
2
3
4
5

# 使用限制

虽然数字分隔符看起来很简单,但在使用时还是有一些限制。比如你只能在两个数字之间添加 _ 分隔符。以下的使用方式是非法的:

// Numeric separators are not allowed here.(6188)
3_.141592 // Error
3._141592 // Error

// Numeric separators are not allowed here.(6188)
1_e10 // Error
1e_10 // Error

// Cannot find name '_126301'.(2304)
_126301 // Error

// Numeric separators are not allowed here.(6188)
126301_ // Error

// Cannot find name 'b111111000'.(2304)
// An identifier or keyword cannot immediately follow a numeric literal.(1351)
0_b111111000 // Error

// Numeric separators are not allowed here.(6188)
0b_111111000 // Error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

当然你也不能连续使用多个 _ 分隔符,比如:

// Multiple consecutive numeric separators are not permitted.(6189)
123__456 // Error
1
2

# 解析分隔符

此外,需要注意的是以下用于解析数字的函数是不支持分隔符:

  • Number()

  • parseInt()

  • parseFloat()

Number("123_456"); // NaN
parseInt("123_456"); // 123
parseFloat("123_456"); // 123
1
2
3

要解决上述问题,只需要非数字的字符删掉即可。这里我们来定义一个 removeNonDigits 的函数:

const RE_NON_DIGIT = /[^0-9]/gu;
function removeNonDigits(str) {
  str = str.replace(RE_NON_DIGIT, "");
  return Number(str);
}
1
2
3
4
5

该函数通过调用字符串的 replace 方法来移除非数字的字符:

removeNonDigits("123_456"); // 123456
removeNonDigits("149,600,000"); // 149600000
removeNonDigits("1,407,836"); // 1407836
1
2
3

# 声明合并

声明合并 (opens new window)

# 声明文件

声明文件 (opens new window)

# TypeScript 核心库的定义文件

TypeScript 核心库的定义文件 (opens new window)中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。当我们在使用一些常用方法的时候,TypeScript 实际上已经帮我们做了很多类型判断的工作了,我们可以直接使用。

注意

TypeScript 核心库的定义中不包含 Node.js 部分。

# 元组(Tuple)

元组 (opens new window)类型与数组类型⾮常相似,表⽰⼀个已知元素数量和类型的数组,各元素的类型不必相同

let x: [string, number];
x = ["hello", 10, false]; // Error
x = ["hello"]; // Error
1
2
3

元组⾮常严格,即使类型的顺序不⼀样也会报错。

let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error
1
2
3

我们可以把元组看成严格版的数组,⽐如 [string, number] 我们可以看成是:

interface Tuple extends Array<string | number> {
  0: string;
  1: number;
  length: 2;
}
1
2
3
4
5

# 元组越界问题

const tuple: [string, number] = ["a", 1];
tuple.push(2); // ok
console.log(tuple); // ["a", 1, 2] -> 正常打印出来
// console.log(tuple[2]); // Tuple type '[string, number]' of length '2' has no element at index '2'
1
2
3
4

# 标记的元组元素

在以下的示例中,我们使用元组类型来声明剩余参数的类型:

function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
addPerson("lolo", 5); // Person info: name: lolo, age: 5
1
2
3
4

其实,对于上面的 addPerson 函数,我们也可以这样实现:

function addPerson(name: string, age: number) {
  console.log(`Person info: name: ${name}, age: ${age}`);
}
1
2
3

这两种方式看起来没有多大的区别,但对于第一种方式,我们没法设置第一个参数和第二个参数的名称。虽然这样对类型检查没有影响,但在元组位置上缺少标签,会使得它们难于使用。

为了提高开发者使用元组的体验,TypeScript 4.0 支持为元组类型设置标签:

function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
1
2
3

之后,当我们使用 addPerson 方法时,TypeScript 的智能提示就会变得更加友好。

// 未使用标签的智能提示
// addPerson(args_0: string, args_1: number): void
function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}
// 已使用标签的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
1
2
3
4
5
6
7
8
9
10

# 枚举

枚举 (opens new window)类型是很多语⾔都拥有的类型,它⽤于声明⼀组命名的常数,当⼀个变量有⼏种可能的取值时,可以将它定义为枚举类型

# 数字枚举

当我们声明⼀个枚举类型时,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,⽽且默认从 0 开始依次累加

enum Direction {
  Up,
  Down,
  Left,
  Right
}
console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true
1
2
3
4
5
6
7
8
9
10

当我们把第⼀个值赋值后,后⾯也会根据第⼀个值进⾏累加:

enum Direction {
  Up = 10,
  Down,
  Left,
  Right
}
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13
1
2
3
4
5
6
7

上例的编译结果如下:

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

# 字符串枚举

enum Direction {
  Up = "Up",
  Down = "Down",
  Left = "Left",
  Right = "Right"
}
console.log(Direction["Right"], Direction.Up); // Right Up
1
2
3
4
5
6
7

# 常数枚举

常数枚举是使用 const enum 定义的枚举类型:

const enum Directions {
  Up,
  Down,
  Left,
  Right
}

let directions = [
  Directions.Up,
  Directions.Down,
  Directions.Left,
  Directions.Right
];
1
2
3
4
5
6
7
8
9
10
11
12
13

常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。因此对性能提升有帮助。

上例的编译结果是:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
1

假如包含了计算成员,则会在编译阶段报错:

const enum Color {
  Red,
  Green,
  Blue = "blue".length
}

// index.ts(1,38): error TS2474: In 'const' enum declarations member initializer must be constant expression.
1
2
3
4
5
6
7

# 外部枚举

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

declare enum Directions {
  Up,
  Down,
  Left,
  Right
}

let directions = [
  Directions.Up,
  Directions.Down,
  Directions.Left,
  Directions.Right
];
1
2
3
4
5
6
7
8
9
10
11
12
13

declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

上例的编译结果是:

var directions = [
  Directions.Up,
  Directions.Down,
  Directions.Left,
  Directions.Right
];
1
2
3
4
5
6

外部枚举与声明语句一样,常出现在声明文件中。

同时使用 declare 和 const 也是可以的:

declare const enum Directions {
  Up,
  Down,
  Left,
  Right
}

let directions = [
  Directions.Up,
  Directions.Down,
  Directions.Left,
  Directions.Right
];
1
2
3
4
5
6
7
8
9
10
11
12
13

编译结果:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
1

在常规枚举中,如果其前面的枚举成员被视为常量,则没有初始值设定项的成员将被视为常量。相比之下,没有初始值设定项的外部枚举成员始终被视为计算成员。

# 异构枚举

异构枚举的成员值是数字和字符串的组合:

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES"
}
1
2
3
4

通常情况下我们很少会这样使⽤枚举,但是从技术的⻆度来说,它是可⾏的。

# 枚举的反向映射

我们可以通过枚举名字获取枚举值,这当然看起来没问题,那么能不能通过枚举值获取枚举名字呢?答案当然是可以的。

enum Direction {
  Up,
  Down,
  Left,
  Right
}
console.log(Direction[0]); // Up
let a = Direction.Up;
let oneOfDirection = Direction[a]; // Up
1
2
3
4
5
6
7
8
9

注意

字符串枚举成员根本不会生成反向映射。

# 枚举成员的类型

当所有枚举成员都拥有字⾯量枚举值时,它就带有了⼀种特殊的语义,即枚举成员成为了类型。

enum Direction {
  Up,
  Down,
  Left,
  Right
}
const a = 0;
console.log(a === Direction.Up); // true
1
2
3
4
5
6
7
8

我们把成员当做值使⽤,看来是没问题的,因为成员值本⾝就是 0,那么我们再加⼏⾏代码:

type c = 0;
declare let b: c;
b = 1; // 不能将类型 “1” 分配给类型 “0”
b = Direction.Up; // ok
1
2
3
4

# 联合枚举类型

enum Direction {
  Up,
  Down,
  Left,
  Right
}
declare let a: Direction;

enum Animal {
  Dog,
  Cat
}

a = Direction.Up; // ok
a = Animal.Dog; // 不能将类型 “Animal.Dog” 分配给类型 “Direction”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们把 a 声明为 Direction 类型,可以看成我们声明了⼀个联合类型 Direction.Up | Direction.Down | Direction.Left | Direction.Right,只有这四个类型其中的成员才符合要求。

# 类(Class)

传统的⾯向对象语⾔基本都是基于类的,JavaScript 通过构造函数实现 (opens new window)的概念,通过原型链实现继承。在 ES6 之后,JavaScript 拥有了 class 关键字,虽然本质依然是构造函数,但是开发者已经可以⽐较舒服地使⽤ class 了。

# 静态属性和静态方法

class Greeter {
  // 静态属性
  static cname: string = "Greeter"; // 成员属性
  greeting: string;

  // 构造函数 - 执行初始化操作
  constructor(message: string) {
    this.greeting = message;
  }
  // 静态方法
  static getClassName() {
    return "Class name is Greeter";
  }
  // 成员方法
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greeter = new Greeter("world");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

编译生成的 es5 代码如下:

"use strict";
var Greeter = /** @class */ (function() {
  // 构造函数 - 执行初始化操作
  function Greeter(message) {
    this.greeting = message;
  }
  // 静态方法
  Greeter.getClassName = function() {
    return "Class name is Greeter";
  };
  // 成员方法
  Greeter.prototype.greet = function() {
    return "Hello, " + this.greeting;
  };
  // 静态属性
  Greeter.cname = "Greeter";
  return Greeter;
})();
var greeter = new Greeter("world");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 抽象类

抽象类作为其它派⽣类的基类使⽤,它们⼀般不会直接被实例化,不同于接⼝,抽象类可以包含成员的实现细节。

abstract 关键字是⽤于定义抽象类和在抽象类内部定义抽象⽅法。

⽐如我们创建⼀个 Animal 抽象类:

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

如果实例化此抽象类会报错,我们不能直接实例化抽象类,通常需要我们创建⼦类继承基类,然后可以实例化⼦类。

class Cat extends Animal {
  makeSound() {
    console.log("miao miao");
  }
}

const cat = new Cat();

cat.makeSound(); // miao miao
cat.move(); // roaming the earch...
1
2
3
4
5
6
7
8
9
10

# 访问修饰符

📌 public

在 TypeScript 的类中,成员都默认为 public,被它修饰的成员是可以被外部访问

📌 private

当成员被设置为 private 之后,被它修饰的成员是只可以被类的内部访问

📌 protected

当成员被设置为 protected 之后, 被它修饰的成员是只可以被类的内部以及类的⼦类访问

# 类可以作为接口

实际上类(class)也可以作为接⼝,⽽把 class 作为 interface 使⽤,在 React ⼯程中是很常⽤的。

由于组件需要传⼊ props 的类型 Props ,同又有需要设置默认 props 即 defaultProps 。 这个时候 class 作为接⼝的优势就体现出来了。

我们先声明⼀个类,这个类包含组件 props 所需的类型和初始值:

// props 的类型
export default class Props {
  public children:
    | Array<React.ReactElement<any>>
    | React.ReactElement<any>
    | never[] = [];
  public speed: number = 500;
  public height: number = 160;
  public animation: string = "easeInOutQuad";
  public isAuto: boolean = true;
  public autoPlayInterval: number = 4500;
  public afterChange: () => {};
  public beforeChange: () => {};
  public selesctedColor: string;
  public showDots: boolean = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当我们需要传⼊ props 类型的时候直接将 Props 作为接⼝传⼊,此时 Props 的作⽤就是接⼝,⽽当需要我们设置 defaultProps 初始值的时候,我们只需要:

public static defaultProps = new Props()
1

Props 的实例就是 defaultProps 的初始值,这就是 class 作为接⼝的实际应⽤,我们⽤⼀个 class 起到了接⼝和设置初始值两个作⽤,⽅便统⼀管理,减少了代码量

# 类与接口

类与接口 (opens new window)

# ECMAScript 私有字段

在 TypeScript 3.8 版本就开始支持 ECMAScript 私有字段,使用方式如下:

class Person {
  #name: string;

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

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称;

  • 每个私有字段名称都唯一地限定于其包含的类;

  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);

  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

使用 # 号定义的 ECMAScript 私有字段,会通过 WeakMap 对象来存储,同时编译器会生成 __classPrivateFieldSet__classPrivateFieldGet 这两个方法用于设置值和获取值。

# 构造函数的类属性推断

noImplicitAny 配置属性被启用之后,TypeScript 4.0 就可以使用控制流分析来确认类中的属性类型:

class Person {
  fullName; // (property) Person.fullName: string
  firstName; // (property) Person.firstName: string
  lastName; // (property) Person.lastName: string
  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName = fullName.split(" ")[1];
  }
}
1
2
3
4
5
6
7
8
9
10

然而对于以上的代码,如果在 TypeScript 4.0 以前的版本,比如在 3.9.2 版本下,编译器会提示以下错误信息:

class Person {
  // Member 'fullName' implicitly has an 'any' type.(7008)
  fullName; // Error
  firstName; // Error
  lastName; // Error
  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName = fullName.split(" ")[1];
  }
}
1
2
3
4
5
6
7
8
9
10
11

从构造函数推断类属性的类型,该特性给我们带来了便利。但在使用过程中,如果我们没法保证对成员属性都进行赋值,那么该属性可能会被认为是 undefined。

class Person {
  fullName; // (property) Person.fullName: string
  firstName; // (property) Person.firstName: string | undefined
  lastName; // (property) Person.lastName: string | undefined
  constructor(fullName: string) {
    this.fullName = fullName;
    if (Math.random()) {
      this.firstName = fullName.split(" ")[0];
      this.lastName = fullName.split(" ")[1];
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 泛型(Generics)

泛型(Generics) (opens new window)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

function returnItem<T>(para: T): T {
  return para;
}
1
2
3

# 泛型的应用场景

通常在决定是否使用泛型时,我们有以下两个参考标准:

  • 当你的函数、接口或类将处理多种数据类型时;

  • 当函数、接口或类在多个地方使用该数据类型时。

# 多个类型参数

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

swap([7, "seven"]); // ['seven', 7]
1
2
3
4
5

# 泛型约束

泛型约束主要有以下应用场景:

📌 1. 确保属性存在

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

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

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
1
2
3
4
5
6

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface Lengthwise {
  length: number;
}

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

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity(7);

// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
1
2
3
4
5
6
7
8
9
10
11
12

多个类型参数之间也可以互相约束:

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = (<T>source)[id];
  }
  return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });
1
2
3
4
5
6
7
8
9
10

这里我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

📌 2. 检查对象上的键是否存在

比如要实现一个获取对象属性的函数 getProperty:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
1
2
3

我们通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key]; 不同。

getProperty 函数的具体使用方式如下:

enum Difficulty {
  Easy,
  Intermediate,
  Hard
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
let tsInfo = {
  name: "Typescript",
  supersetOf: "Javascript",
  difficulty: Difficulty.Intermediate
};
let difficulty: Difficulty = getProperty(tsInfo, "difficulty"); // OK
let supersetOf: string = getProperty(tsInfo, "superset_of"); // Error, 类型 "superset_of" 的参数不能赋给类型 "difficulty" | "name" | "supersetOf" 的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。

📌 3. 解决误将泛型当作 jsx 标签的问题

在 .tsx ⽂件⾥,泛型可能会被当做 jsx 标签。

const toArray = <T>(element: T) => [element]; // Error in .tsx file.
1

此时使用泛型约束即可解决。

const toArray = <T extends {}>(element: T) => [element]; // No errors.
1

# 泛型接⼝

interface ReturnItemFn<K> {
  (para: K): K;
}

const returnItem: ReturnItemFn<number> = (para) => para;
1
2
3
4
5

# 泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();

myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
  return x + y;
};
1
2
3
4
5
6
7
8
9
10
11

# 泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}
1
2
3
4
5
6
7

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的。

  • 必选的类型参数不能在可选的类型参数后。

  • 如果类型参数有约束,类型参数的默认类型必须满足这个约束。

  • 当指定类型实参时,你只需要指定必选类型参数的类型实参。

  • 未指定的类型参数会被解析为它们的 默认类型。

  • 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。

  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。

  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型。

# 泛型条件类型

在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。

条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y
1

以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取:

interface Dictionary<T = any> {
  [key: string]: T;
}

type StrDict = Dictionary<string>;

type DictMember<T> = T extends Dictionary<infer V> ? V : never;
type StrDictMember = DictMember<StrDict>; // string
1
2
3
4
5
6
7
8

在这个示例中,当类型 T 满足 T extends Dictionary 约束时,我们会使用 infer 关键字声明了一个类型变量 V,并返回该类型,否则返回 never 类型。

除了上述的应用外,利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型,比如:

async function stringPromise() {
  return "Hello, Semlinker!";
}

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

async function personPromise() {
  return { name: "Semlinker", age: 30 } as Person;
}

type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;

type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 常用关键字

# typeof

在 TypeScript 中, typeof 操作符可以用来获取一个变量声明或对象的类型。

interface Person {
  name: string;
  age: number;
}
const sem: Person = { name: "semlinker", age: 33 };
type Sem = typeof sem; // -> Person
function toArray(x: number): Array<number> {
  return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
1
2
3
4
5
6
7
8
9
10

# keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

interface Person {
  name: string;
  age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
1
2
3
4
5
6
7

在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string;
}
interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}
1
2
3
4
5
6
7
8

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数字索引时,JavaScript 在执行索引操作时,会先把数字索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number 。

# in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c";
typeObj = {
  [p in Keys]: any
}; // -> { a: any, b: any, c: any }
1
2
3
4

# infer

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

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

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。在实际开发中可以这么使用:

interface User {
  id: number;
  name: string;
  form?: string;
}
type Foo = () => User;
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
type R = ReturnType<Foo>; // User
1
2
3
4
5
6
7
8

📌 2. 可以利用 infer 将元组转成联合类型:

type ElementOf<T> = T extends Array<infer E> ? E : never;
type TTuple = [string, number];
type ToUnion = ElementOf<TTuple>; // string | number
1
2
3

📌 3. 可以利用 infer 来获取一个类的构造函数的参数类型:

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

// new (...args: any[]) => any 指构造函数, 因为构造函数是可以被实例化的.
// infer P 代表待推断的构造函数参数, 如果接受的类型 T 是一个构造函数, 那么返回构造函数的参数类型 P, 否则什么也不返回, 即 never 类型
type ConstructorParameters<
  T extends new (...args: any[]) => any
> = T extends new (...args: infer P) => any ? P : never;

type R = ConstructorParameters<typeof TestClass>; // [name: string, age: number]
1
2
3
4
5
6
7
8
9
10
11

# extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
1
2
3
4
5
6
7

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3); // Error, number doesn't have a .length property
1

这时我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({ length: 10, value: 3 });
1

更多用法可以参考:Typescript 中的 extends 关键字 (opens new window)

# is

is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。

is 关键字经常用来封装 “类型判断函数”,通过和函数返回值的比较,从而缩小参数的类型范围,所以类型谓词 is 也是一种类型保护。

function isString(test: any): test is string {
  return typeof test === "string";
}

function example(foo: number | string) {
  if (isString(foo)) {
    console.log("it is a string" + foo);
    console.log(foo.length); // string function
  } else {
    console.log(foo);
  }
}
example("hello world");
1
2
3
4
5
6
7
8
9
10
11
12
13

常用的类型判断函数:

const isNumber = (val: unknown): val is number => typeof val === "number";
const isString = (val: unknown): val is string => typeof val === "string";
const isSymbol = (val: unknown): val is symbol => typeof val === "symbol";
const isFunction = (val: unknown): val is Function => typeof val === "function";
const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === "object";

function isPromise<T = any>(val: unknown): val is Promise<T> {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch);
}

const objectToString = Object.prototype.toString;
const toTypeString = (value: unknown): string => objectToString.call(value);
const isPlainObject = (val: unknown): val is object =>
  toTypeString(val) === "[object Object]";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 交叉类型(&)

交叉类型是将多个类型合并为⼀个类型。通过 & 运算符可以把现有的多种类型叠加到⼀起成为⼀种类型,它包含了所需的所有类型的特性。

这跟 JavaScript 中的混⼊模式很类似,在这种模式中,你可以从两个对象中创建⼀个新对象,新对象会拥有着两个对象所有的功能。

function mixin<T extends object, U extends object>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let id in first) {
    (<T>result)[id] = first[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<U>result)[id] = second[id];
    }
  }
  return result;
}

const x = mixin({ a: "hello" }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
console.log(x.a);
console.log(x.b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 同名基础类型属性的合并

假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,比如:

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string;
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 string 或 number 类型呢?比如下面的例子:

p = { c: 6, d: "d", e: "e" }; // 不能将类型“number”分配给类型“never”。ts(2322)

q = { c: "c", d: "d", e: "e" }; // 不能将类型“number”分配给类型“never”。ts(2322)
1
2
3

为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never 呢?这是因为混入后成员 c 的类型为 string & number,即成员 c 的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never。

# 同名非基础类型属性的合并

如果是非基本数据类型的话,又会是什么情形。比如:

interface D {
  d: boolean;
}
interface E {
  e: string;
}
interface F {
  f: number;
}

interface A {
  x: D;
}
interface B {
  x: E;
}
interface C {
  x: F;
}

type ABC = A & B & C;

let abc: ABC = {
  x: {
    d: true,
    e: "semlinker",
    f: 666
  }
};
console.log("abc:", abc);
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

以上代码可以正常输出预期的结果。由此可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并

# 字⾯量类型

字⾯量(Literal Type)主要分为真值字⾯量类型(boolean literal types)、数字字⾯量类型(numeric literal types)、枚举字⾯量类型(enum literal types)、⼤整数字⾯量类型(bigInt literal types)和字符串字⾯量类型(string literal types)。

const a: 2333 = 2333; // ok
const ab: 0b10 = 2; // ok
const ao: 0o114 = 0b1001100; // ok
const ax: 0x514 = 0x514; // ok
const b: 0x1919n = 6425n; // ok
const c: "xiaoniao" = "xiaoniao"; // ok
const d: false = false; // ok
const g: "github" = "pronhub"; // 不能将类型 "pronhub" 分配给类型 "github"
1
2
3
4
5
6
7
8

当字⾯量类型与联合类型结合的时候,⽤处就显现出来了,它可以模拟⼀个类似于枚举的效果:

type Direction = "North" | "East" | "South" | "West";

function move(distance: number, direction: Direction) {
  // ...
}
1
2
3
4
5

# 字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

// 使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种
type EventNames = "click" | "scroll" | "mousemove";
function handleEvent(ele: Element, event: EventNames) {
  // do something
}

handleEvent(document.getElementById("hello"), "scroll"); // 没问题
handleEvent(document.getElementById("world"), "dblclick"); // 报错,event 不能为 'dblclick'

// index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.
1
2
3
4
5
6
7
8
9
10

注意

类型别名与字符串字面量类型都是使用 type 进行定义。

# 类型字⾯量

类型字⾯量(Type Literal)不同于字⾯量类型(Literal Type),它跟 JavaScript 中的对象字⾯量的语法很相似:

type Foo = {
  baz: [number, "xiaoniao"];
  toString(): string;
  readonly [Symbol.iterator]: "github";
  0x1: "foo";
  bar: 12n;
};
1
2
3
4
5
6
7

# 类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。

目前主要有四种的方式来实现类型保护:

# in 关键字

interface Admin {
  name: string;
  privileges: string[];
}
interface Employee {
  name: string;
  startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# typeof 关键字

typeof 类型保护只支持两种形式: typeof v === "typename" 和 typeof v !== typename ,"typename" 必须是 "number","string","boolean" 或 "symbol"。但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}
1
2
3
4
5
6
7
8
9

# instanceof 关键字

interface Padder {
  getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}
class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === "number";
}
function isString(x: any): x is string {
  return typeof x === "string";
}
1
2
3
4
5
6

# 类型兼容性

# 结构类型

TypeScript ⾥的类型兼容性是基于「结构类型」的,结构类型是⼀种只使⽤其成员来描述类型的⽅式,其基本规则是,如果 x 要兼容 y,那么 y ⾄少具有与 x 相同的属性

我们做⼀个简单的实验,我们构建⼀个类 Person,然后声明⼀个接⼝ Dog, Dog 的属性 Person 都拥有,⽽且还多了其他属性,这种情况下 Dog 兼容了 Person 。

class Person {
  constructor(
    public weight: number,
    public name: string,
    public born: string
  ) {}
}
interface Dog {
  name: string;
  weight: number;
}

let x: Dog;
x = new Person(120, "cxk", "1996-12-12"); // OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14

但反过来就不⾏。

# 函数的类型兼容性

函数类型的兼容性判断,要查看 x 是否能赋值给 y,⾸先看它们的参数列表。

x 的每个参数必须能在 y ⾥找到对应类型的参数,需要注意的是,参数的名字相同与否⽆所谓,只看它们的类型。

let q = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = q; // OK
q = y; // Error 不能将类型 “(b: number, s: string) => number” 分配给类型 “(a: number) => number”。
1
2
3
4
5
let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let bas = (...args: number[]) => {};

// 当我们把 strictNullChecks 设置为 false 时,下述代码是兼容的。
foo = bar = bas; // Error
bas = bar = foo; // Error
1
2
3
4
5
6
7
let foo2 = (x: number, y: number) => {};
let bar2 = (x?: number) => {};

foo2 = bar2; // ok
bar2 = foo2; // Error
1
2
3
4
5

# 类的类型兼容性

// 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查
class Animal2 {
  feet: number;
  constructor(name: string, numFeet: number) {
    this.feet = numFeet;
  }
}

class Size {
  feet: number;
  constructor(meters: number) {
    this.feet = meters;
  }
}

let a: Animal2 = new Animal2("a", 2);
let s: Size = new Size(1);

a = s; // OK
s = a; // OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 泛型的类型兼容性

泛型本⾝就是不确定的类型,它的表现根据是否被成员使⽤⽽不同。

interface Person<T> {}

let x: Person<string>;
let y: Person<number>;

x = y; // ok
y = x; // ok
1
2
3
4
5
6
7

由于没有被成员使⽤泛型,所以这⾥是没问题的。

那么我们再看下⾯:

interface Person<T> {
  name: T;
}

let x: Person<string>;
let y: Person<number>;

x = y; // 不能将类型 “Person<number>” 分配给类型 “Person<string>”。
y = x; // 不能将类型 “Person<string>” 分配给类型 “Person<number>”。
1
2
3
4
5
6
7
8
9

# 装饰器

# 什么是装饰器

  • 它是一个表达式。

  • 该表达式被执行后,返回一个函数。

  • 函数的入参分别为 target、name 和 descriptor。

  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象。

# 启用实验性的装饰器特性

若要启用实验性的装饰器特性,必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项。

命令行:

tsc --target ES5 --experimentalDecorators
1

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}
1
2
3
4
5
6

# 类装饰器

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:

  • target: TFunction - 被装饰的类

类装饰器声明:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;
1
2
3

具体例子:

function Greeter(target: Function): void {
  target.prototype.greet = function(): void {
    console.log("Hello Semlinker!");
  };
}
@Greeter
class Greeting {
  constructor() {
    // 内部实现
  }
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';
1
2
3
4
5
6
7
8
9
10
11
12
13

如果想要自定义输出的文案,可以这么写:

function Greeter(greeting: string) {
  return function(target: Function) {
    target.prototype.greet = function(): void {
      console.log(greeting);
    };
  };
}
@Greeter("Hello TS!")
class Greeting {
  constructor() {
    // 内部实现
  }
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 属性装饰器

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:

  • target: Object - 被装饰的类

  • propertyKey: string | symbol - 被装饰类的属性名

属性装饰器声明:

declare type PropertyDecorator = (
  target: Object,
  propertyKey: string | symbol
) => void;
1
2
3
4

具体例子:

function logProperty(target: any, key: string) {
  delete target[key];
  const backingField = "_" + key;
  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });
  // property getter
  const getter = function(this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };
  // property setter
  const setter = function(this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };
  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}
class Person {
  @logProperty
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
}
const p1 = new Person("semlinker");
p1.name = "kakuqo";
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

以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:

Set: (name) => semlinker;
Set: (name) => kakuqo;
1
2

# 方法装饰器

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:

  • target: Object - 被装饰的类

  • propertyKey: string | symbol - 方法名

  • descriptor: TypePropertyDescript - 属性描述符

方法装饰器声明:

declare type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypePropertyDescript<T>
) => TypedPropertyDescriptor<T> | void;
1
2
3
4
5

具体例子:

function log(
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  let originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log("wrapped function: before invoking " + propertyKey);
    let result = originalMethod.apply(this, args);
    console.log("wrapped function: after invoking " + propertyKey);
    return result;
  };
}
class Task {
  @log
  runTask(arg: any): any {
    console.log("runTask invoked, args: " + arg);
    return "finished";
  }
}
let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

以上代码成功运行后,控制台会输出以下结果:

"wrapped function: before invoking runTask";
"runTask invoked, args: learn ts";
"wrapped function: after invoking runTask";
"result: finished";
1
2
3
4

# 参数装饰器

参数装饰器顾名思义,是用来装饰函数参数的,它接收三个参数:

  • target: Object - 被装饰的类

  • propertyKey: string | symbol - 方法名

  • parameterIndex: number - 方法中参数的索引值

参数装饰器声明:

declare type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;
1
2
3
4
5

具体例子:

function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(
    `The parameter in position ${parameterIndex} at ${functionLogged} has been decorated`
  );
}
class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

以上代码成功运行后,控制台会输出以下结果:

"The parameter in position 0 at Greeter has been decorated";
1

# 常用内置工具类型

为了方便开发者,TypeScript 内置了一些常用的工具类型 (opens new window),比如 Partial、Required、Readonly、Record 和 ReturnType 等。这里是它们的源码实现 (opens new window)

# DOM 元素类型

HTMLElementTagNameMap (opens new window)

# 事件类型列表

List of event types (opens new window)

注意

事件类型列表中并没有 InputEvent。这是因为 Typescript 不支持它,因为事件本身没有完全的浏览器支持,并且在不同的浏览器中可能表现不同。可以用 KeyboardEvent 代替。

# tsconfig.json

了不起的 tsconfig.json 指南 (opens new window)

# tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径;

  • 用于配置 TypeScript 编译器;

  • 用于指定编译的文件。

# tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;

  • include - 设置需要进行编译的文件,支持路径模式匹配;

  • exclude - 设置无需进行编译的文件,支持路径模式匹配;

  • compilerOptions - 设置与编译流程相关的选项。

# compilerOptions 选项

compilerOptions 支持很多选项,常⻅的有 baseUrl 、 target 、 moduleResolution 和 lib 等。

compilerOptions 每个选项的详细说明如下:

{
  "compilerOptions": {
    /* 基本选项 */
    "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [], // 指定要包含在编译中的库文件
    "allowJs": true, // 允许编译 javascript 文件
    "checkJs": true, // 报告 javascript 文件中的错误
    "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true, // 生成相应的 '.d.ts' 文件
    "sourceMap": true, // 生成相应的 '.map' 文件
    "outFile": "./", // 将输出文件合并为一个文件
    "outDir": "./", // 指定输出目录
    "rootDir": "./", // 用来控制输出目录结构 --outDir.
    "removeComments": true, // 删除编译后的所有的注释
    "noEmit": true, // 不生成输出文件
    "importHelpers": true, // 从 tslib 导入辅助工具函数
    "isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true, // 启用所有严格类型检查选项
    "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true, // 启用严格的 null 检查
    "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true, // 有未使用的变量时,抛出错误
    "noUnusedParameters": true, // 有未使用的参数时,抛出错误
    "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */

    "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./", // 用于解析非相对模块名称的基目录
    "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [], // 包含类型声明的文件列表
    "types": [], // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而
    "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件
    "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将
    "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件 中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true, // 启用装饰器
    "emitDecoratorMetadata": 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

# TypeScript 开发辅助工具

# TypeScript Playground

TypeScript 官方提供的在线 TypeScript 运行环境 (opens new window),利用它可以方便地学习 TypeScript 相关知识与不同版本的功能特性。

除了 TypeScript 官方的 Playground 之外,也可以选择其他的 Playground,比如 codepen.io (opens new window)stackblitz (opens new window)jsbin.com (opens new window) 等。

# TypeScript UML Playground

一款在线 TypeScript UML 工具 (opens new window),可以利用它为指定的 TypeScript 代码生成 UML 类图。

# quicktype

quicktype (opens new window) 能够将 JSON 转换为任何语言的代码,比如将 JSON 转换为 typescript。

# TypeSearch

可以在这里 (opens new window)查找具有类型声明的 npm 包,无论是捆绑的还是绝对类型的。

# Schemats

利用 Schemats (opens new window),你可以基于(Postgres,MySQL)SQL 数据库中的 schema 自动生成 TypeScript 接口定义。

# TypeScript AST Viewer

一款 TypeScript AST 在线工具 (opens new window),利用它你可以查看指定 TypeScript 代码对应的 AST(Abstract Syntax Tree)抽象语法树。

对于了解过 AST 的小伙伴来说,对 astexplorer (opens new window) 这款在线工具应该不会陌生。该工具除了支持 JavaScript 之外,还支持 CSS、JSON、RegExp、GraphQL 和 Markdown 等格式的解析。

# TypeDoc

TypeDoc (opens new window) 用于将 TypeScript 源代码中的注释转换为 HTML 文档或 JSON 模型。它可灵活扩展,并支持多种配置。

# TypeScript ESLint

使用 TypeScript ESLint (opens new window) 可以帮助我们规范代码质量,提高团队开发效率。配置和使用可以参考:在 Typescript 项目中,如何优雅的使用 ESLint 和 Prettier (opens new window)

# Bring your own TypeScript

Bring your own TypeScript (opens new window) 项目通过暴露内部接口让编译器 API 使用起来更简单。你可以在全局范围上暴露你 TypeScript 应用的本地变量。

# tsdx

tsdx (opens new window) 能够帮助我们快速创建可以发布到 npm 上的 react 组件库项目(注意不是应用程序),从而节省各种配置时间。

# TypeScript 编译原理

# 编译器的组成

TypeScript 有⾃⼰的编译器,这个编译器主要有以下部分组成:

  • Scanner 扫描器

  • Parser 解析器

  • Binder 绑定器

  • Emitter 发射器

  • Checker 检查器

# 编译器的处理

  1. 扫描器通过扫描源代码⽣成 token 流:
SourceCode(源码)+ 扫描器 --> Token 流
1
  1. 解析器将 token 流解析为抽象语法树(AST):
Token 流 + 解析器 --> AST(抽象语法树)
1
  1. 绑定器将 AST 中的声明节点与相同实体的其他声明相连形成符号(Symbols),符号是语义系统的主要构造块:
AST + 绑定器 --> Symbols(符号)
1
  1. 检查器通过符号和 AST 来验证源代码语义:
AST + 符号 + 检查器 --> 类型验证
1
  1. 最后我们通过发射器⽣成 JavaScript 代码:
AST + 检查器 + 发射器 --> JavaScript 代码
1

# 编译器处理流程

TypeScript 的编译流程也可以粗略得分为三步:

  • 解析

  • 转换

  • ⽣成

结合上部分的编译器各个组成部分,流程如下图:

mac

# 学习 ts 的四个阶段

  • ts 基础夯实,学习了解 php/java/c# 任意一门语言,同时别忘了收集小技巧,建立自己的 utils

  • 开发 node 阶段

  • 原声 dom 开发,vue,react

  • 开发 sdk 阶段,编译 ts

# 运行 ts 文件的方法

  • 第一种是全局安装了 ts 之后,通过 tsc 命令先将 ts 文件编译成 js 文件,然后再通过 node 命令运行该 js 文件。

  • 第二种是全局安装 ts 和 ts-node (opens new window) 之后,直接通过 ts-node 命令运行 ts 文件。

上次更新时间: 2024年01月04日 22:57:37