TypeScript 学习手册

1.TypeScript 概念

TypeScript(简称 TS,静态类型)是微软公司开发的一种基于 JavaScript (简称 JS,动态类型)语言的编程语言。TypeScript 可以看成是 JavaScript 的超集(superset),即它继承了后者的全部语法,增加了一些自己的语法。使用TypeScript可以帮助开发人员在编码过程中避免一些常见的错误,并提供更好的代码编辑功能和工具支持。

2.数据类型

基础类型

  • number: 表示数字,包括整数和浮点数。
  • bigint: 表示大整数。
  • string: 表示文本字符串。
  • boolean: 表示布尔值。
  • null、undefined: 分别表示null和undefined。
  • symbol: 表示唯一的、不可变的值。

复合类型

  • array: 表示数组,可以使用number[]Array<number>来声明其中元素的类型。
  • tuple: 表示元组,用于表示固定数量和类型的数组。
  • enum: 表示枚举类型,用于定义具名常量集合。

对象类型

  • object: 表示非原始类型,即除number、bigint、string、boolean、symbol、null或undefined之外的类型。
  • interface: 用于描述对象的结构,并且可以重复使用。

函数类型

  • function: 表示函数类型。
  • void: 表示函数没有返回值。

高级类型

  • union types: 联合类型,表示一个值可以是几种类型之一。
  • intersection types: 交叉类型,表示一个值同时拥有多种类型的特性。

顶层类型

  • any: 表示任意类型。
  • unknown: 严格版的 any 类型。

底层类型

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

3.基础用法

类型声明

变量只有赋值后才能使用,否则就会报错

let x: number;
console.log(x); // 报错

只读数组

const arr: readonly number[] = [0, 1];

// TypeScript 提供了两个专门的泛型,用来生成只读数组的类型
const a1: ReadonlyArray<number> = [0, 1];
const a2: Readonly<number[]> = [0, 1];

// 还可以使用 as const 声明为只读
const arr = [0, 1] as const
// 类型为 readonly [1, 2, 3]

只读对象

// 方式一
const myUser: {
  readonly name: string;
} = {
  name: '张三',
};

// 方式二
const myUser = {
  name: '张三',
} as const;

// 方式三
const myUser: Readonly<{
  name: string;
}> = {
  name: '张三',
};

// 方式四, ts会自动推导为方式三的类型限制
const myUser= Object.freeze({
  name: '张三',
});

元组

元组表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。

// 数组
let a: number[] = [1];

// 元组
let t: [number] = [1];

// 元组成员的类型可以添加问号后缀(?),表示该成员是可选的
let a: [number, number?] = [1];

// 使用扩展运算符(...),可以表示不限成员数量的元组
type NamedNums = [string, ...number[]];
const a: NamedNums = ['A', 1, 2];

只读元组

// 写法一
type t = readonly [number, string];

// 写法二
type t = Readonly<[number, string]>;

// 写法三
type t = [1, 'a'] as const
// 类型为 readonly [1, "a"]

包装对象类型与字面量类型

'hello' // 字面量
new String('hello') // 包装对象

TypeScript 对五种原始类型分别提供了大写和小写两种类型。

  • Boolean 和 boolean
  • String 和 string
  • Number 和 number
  • BigInt 和 bigint
  • Symbol 和 symbol

其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量。

const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确

const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错

建议只使用小写类型,不使用大写类型。TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。

Object 类型与 object 类型

大写的 Object 类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值(null、undefined除外),都是 Object 类型,这囊括了几乎所有的值。

另外,空对象{}是 Object 类型的简写形式。

小写的 object 类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。

注意,无论是大写的 Object 类型,还是小写的 object 类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。

const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };

o1.toString() // 正确
o1.foo // 报错

o2.toString() // 正确
o2.foo // 报错

值类型

TypeScript 规定,单个值也是一种类型,称为“值类型”。

let x:'hello';

x = 'hello'; // 正确
x = 'world'; // 报错

// 遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型
// x 的类型是 "https"
const x = 'https';

联合类型

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。

let x:string|number;

// 值类型相结合
let setting:true|false;

// 联合类型的第一个成员前面,也可以加上竖杠|,便于多行书写
let x:
  | 'one'
  | 'two'
  | 'three';

如果一个变量有多种类型,读取该变量时,需要进行“类型缩小”

function printId(
  id:number|string
) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

交叉类型

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。

// 交叉类型的主要用途是表示对象的合成
let obj: { foo: string } & { bar: string };
obj = {
  foo: 'hello',
  bar: 'world',
};

// 交叉类型常常用来为对象类型添加新属性
type A = { foo: number };
type B = A & { bar: number };

type 命令

type命令用来定义一个类型的别名。

type Age = number;

别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便

别名不允许重名。

type Color = 'red';
type Color = 'blue'; // 报错

别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。

type Color = 'red';
if (Math.random() < 0.5) {
  type Color = 'blue';
}

别名支持使用表达式。

type World = 'world';
type Greeting = `hello ${World}`;

typeof 运算符

TypeScript 中的 typeof 运算符与 JavaScript 不同,其返回值是该值的 TypeScript 类型。

const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number

// typeof 的参数只能是标识符,不能是需要运算的表达式
type T = typeof Date(); // 报错

// typeof 命令的参数不能是类型
type Age = number;
type MyAge = typeof Age; // 报错
函数重载

有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载。

TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。

function reverse(str: string): string;
function reverse(arr: any[]): any[];

// 注意,重载的各个类型描述与函数的具体实现之间,不能有其他代码,否则报错
// 另外,类型最宽的声明应该放在最后面

构造函数

class Animal {
  numLegs: number = 4;
}

type AnimalConstructor = new () => Animal;

function create(c: AnimalConstructor): Animal {
  return new c();
}

const a = create(Animal);

另一种类型写法,采用对象形式

type F = {
  new (s: string): object;
};

// 某些函数既是构造函数,又可以当作普通函数使用
type F = {
  new (s: string): object;
  (n?: number): number;
};

4.接口 interface

interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。

简介

(1)对象属性

interface IObj {
  a: number;
  b?: number; // 可选
  readonly c: number; // 只读
}

(2)对象的属性索引

interface MyObj {
  [prop: string]: number;

  a: boolean; // 报错, 不能为 boolean

  [prop: number]: string; // 报错, 不能为 string, 与 number 冲突
  [prop: number]: number; // 正确, 数值属性名会自动转换成字符串属性名
}

// 属性的数值索引,可以指定数组的类型
interface A {
  [prop: number]: string;
}
const obj: A = ['a', 'b', 'c'];

(3)对象的方法

// 写法一
interface A {
  f(x: boolean): string;
}

// 写法二
interface B {
  f: (x: boolean) => string;
}

// 写法三
interface C {
  f: { (x: boolean): string };
}

// 属性名可以采用表达式
const f = 'f';
interface A {
  [f](x: boolean): string;
}

// 类型方法可以重载
interface A {
  f(): number;
  f(x: boolean): boolean;
  f(x: string, y: string): string;
}

(4)函数

interface 也可以用来声明独立的函数。

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

const myAdd: Add = (x, y) => x + y;

(5)构造函数

interface 内部可以使用 new 关键字,表示构造函数。

interface ErrorConstructor {
  new (message?: string): Error;
}

继承

(1)interface 继承 interface

interface Style {
  color: string;
}

interface Shape {
  name: string;
}

// 单个继承, 这里 Circle1 是子接口,Shape 是父接口
interface Circle1 extends Shape {
  radius: number;
}

// 多重继承
interface Circle2 extends Style, Shape {
  radius: number;
}

// 子接口与父接口的同名属性必须是类型兼容的
interface Circle1 extends Shape {
  name: number; // 报错
}
// 多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突

(2)interface 继承 type

type Country = {
  name: string;
  capital: string;
};

interface CountryWithPop extends Country {
  population: number;
}
// 注意,如果type命令定义的类型不是对象,interface 就无法继承。

(3)interface 继承 class

class A {
  x: string = '';

  y(): boolean {
    return true;
  }
}

// 继承该类的所有成员
interface B extends A {
  z: number;
}

// 某些类拥有私有(private)成员和保护(protected)成员,interface 可以继承这样的类,
// 但是意义不大,因为私有成员和保护成员只能在类内部访问,无法在类外部使用。

接口合并

多个同名接口会合并成一个接口。

interface Box {
  height: number;
}
interface Box {
  width: number;
  // 属性名相同时, 不能冲突
  height: string; // 报错, 类型冲突
}

// --------------------------
// 对全局对象或者外部库,添加自己的属性和方法
interface Document {
  foo: string;
}
document.foo = 'hello';

// --------------------------
// 同名方法有不同的类型声明,那么会发生函数重载
// 后面的定义比前面的定义具有更高的优先级
interface Cloner {
  clone(animal: Animal): Animal;
}
interface Cloner {
  clone(animal: Sheep): Sheep;
}
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}
// 等同于
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
}

// 例外:同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级
// 类型越具体, 优先级越高
interface A {
  f(x: 'foo'): boolean;
}
interface A {
  f(x: string): void;
  f(x: any): void;
}
// 等同于
interface A {
  f(x: 'foo'): boolean;
  f(x: string): void;
  f(x: any): void;
}

// --------------------------
// 若两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型
interface Circle {
  area: bigint;
}
interface Rectangle {
  area: number;
}
declare const s: Circle | Rectangle;
s.area; // bigint | number

interface 与 type 的异同

很多对象类型既可以用 interface 表示,也可以用 type 表示。

区别有下面几点:

  • (1)type 能够表示非对象类型,而 interface 只能表示对象类型(包括数组、函数等)。
  • (2)interface 可以继承其他类型,type 不支持继承。

继承的主要作用是添加属性,type 定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。

type Animal = {
  name: string;
};
type Bear = Animal & {
  honey: boolean;
};

// interface 可以继承 type
type Foo = { x: number };
interface Bar extends Foo {
  y: number;
}

// type 也可以继承 interface
interface Foo {
  x: number;
}
type Bar = Foo & { y: number };
  • (3)同名 interface 会自动合并,同名 type 则会报错。
  • (4)interface 不能包含属性映射(mapping),type 可以。
interface Point {
  x: number;
  y: number;
}
// 正确
type PointCopy1 = {
  [Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
  [Key in keyof Point]: Point[Key];
};
  • (5)this 关键字只能用于 interface
// 正确
interface Foo {
  add(num: number): this;
}
// 报错
type Foo = {
  add(num: number): this;
};
  • (6)type 可以扩展原始数据类型,interface 不行。
// 正确
type MyStr = string & {
  type: 'new';
};
// 报错
interface MyStr extends string {
  type: 'new';
}
  • (7)interface 无法表达某些复杂类型(比如交叉类型和联合类型),但是 type 可以。
type A = {
  /* ... */
};
type B = {
  /* ... */
};
type AorB = A | B; // 联合类型
// 交叉类型
type AorBwithName = AorB & {
  name: string;
};

5.类 Class

class Point {
  readonly x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x; // 构造方法内部可以设置只读属性的初值
    this.y = y;
  }

  add(point: Point) {
    return new Point(this.x + point.x, this.y + point.y);
  }
}

存取器方法

存取器包括取值器(getter)和存值器(setter)两种方法。

class C {
  _name = '';
  get name() {
    return this._name;
  }
  set name(value) {
    this._name = value;
  }
  // 没有set方法,那么该属性自动成为只读属性
}

implements 关键字

interface Country {
  name: string;
  capital: string;
}
// 或者
type Country = {
  name: string;
  capital: string;
};
// 或者
class Country {
  name: string;
  capital: string;
}

// 实现接口, 实现多个接口用逗号
class MyCountry implements Country {
  name = '';
  capital = '';
}

类与接口的合并

如果一个类和一个接口同名,那么接口会被合并进类。

class A {
  x: number = 1;
}

interface A {
  y: number;
}

let a = new A();
a.y = 10;

a.x; // 1
a.y; // 10

Class 实例类型

可以声明类型为 Class,也可以声明类型为 Interface。

interface MotorVehicle {}

class Car implements MotorVehicle {}

// 写法一
const c1: Car = new Car();
// 写法二
const c2: MotorVehicle = new Car();

类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。

class A {
  greet() {
    console.log('Hello, world!');
  }
}

class B extends A {
  // 子类可以覆盖基类的同名方法,
  // 注意, 子类的同名方法不能与基类的类型定义相冲突, 这里参数不能写成必填参数
  greet(name?: string) {
    if (name === undefined) {
      super.greet(); // 调用基类方法
    } else {
      console.log(`Hello, ${name}`);
    }
  }
}

const b = new B();
b.greet(); // "Hello, world!"

可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected

private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。

protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。

实例属性的简写形式

通过构造方法传入

class Point {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

TypeScript 提供了一种简写形式

class Point {
  constructor(public x: number, public y: number) {}
}

静态成员

类的内部可以使用static关键字,定义静态成员。

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}

MyClass.x; // 0
MyClass.printX(); // 0

static关键字前面可以使用 public、private、protected 修饰符。

泛型类

class Box<Type> {
  contents: Type;

  constructor(value: Type) {
    this.contents = value;
  }
}

const b: Box<string> = new Box('hello!');

注意,静态成员不能使用泛型的类型参数。

抽象类,抽象成员

在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”。

abstract class A {
  id = 1;
}

const a = new A(); // 报错

抽象类只能当作基类使用,用来在它的基础上定义子类。

抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有 abstract 关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

abstract class A {
  abstract foo: string;
  bar: string = '';
}

class B extends A {
  foo = 'b';
}

几个注意点:

  • (1)抽象成员只能存在于抽象类,不能存在于普通类。

  • (2)抽象成员不能有具体实现的代码。

  • (3)抽象成员前也不能有 private 修饰符,否则无法在子类中实现该成员。

  • (4)一个子类最多只能继承一个抽象类。

6.泛型

泛型的特点就是带有“类型参数”(type parameter)。

// <T>,就是类型参数
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

函数的泛型写法

function id<T>(arg: T): T {
  return arg;
}

// 变量形式定义的函数
// 写法一
let myId: <T>(arg: T) => T = id;

// 写法二
let myId: { <T>(arg: T): T } = id;

接口的泛型写法

interface Box<Type> {
  contents: Type;
}

let box: Box<string>;

// 泛型接口还有第二种写法
interface Fn {
  <Type>(arg: Type): Type;
}

function id<Type>(arg: Type): Type {
  return arg;
}

let myId: Fn = id;

类的泛型写法

class Pair<K, V> {
  key: K;
  value: V;
}

类型别名的泛型写法

type Nullable<T> = T | undefined | null;

type Container<T> = { value: T };

类型参数的默认值

function getFirst<T = string>(arr: T[]): T {
  return arr[0];
}

数组的泛型表示

数组类型有一种表示方法是Array<T>ReadonlyArray<T>接口,表示只读数组。

let arr: Array<number> = [1, 2, 3];

let arr: ReadonlyArray<number> = [1, 2, 3];

类型参数的约束条件

// 约束参数必须有 length 属性
function comp<T extends { length: number }>(a: T, b: T) {
  if (a.length >= b.length) {
    return a;
  }
  return b;
}

comp([1, 2], [1, 2, 3]); // 正确
comp('ab', 'abc'); // 正确
comp(1, 2); // 报错

类型参数的约束条件采用下面的形式。

<TypeParameter extends ConstraintType>

类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。

type Fn<A extends string, B extends string = 'world'> = [A, B];

type Result = Fn<'hello'>; // ["hello", "world"]

7.枚举 Enum

简介

Enum 结构,用来将相关常量放在一个容器里面,方便使用。

enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}
// 第一个成员的值默认为整数0,第二个为1,第三个为2,以此类推。

let c = Color.Green; // 1
// 等同于
let c = Color['Green']; // 1

// 类型可以是 Color,也可以是number。
let c: Color = Color.Green; // 正确
let c: number = Color.Green; // 正确

// Enum 既是一种类型,也是一个值
// 编译后
let Color = {
  Red: 0,
  Green: 1,
  Blue: 2,
};

Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

Enum 成员的值

可以为 Enum 成员显式赋值。

// 如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
enum Color {
  Red = 7,
  Green, // 8
  Blue, // 9
}

// 或者
enum Color {
  Red, // 0
  Green = 7,
  Blue, // 8
}

Enum 成员值都是只读的,不能重新赋值。

enum Color {
  Red,
  Green,
  Blue,
}

Color.Red = 4; // 报错

在 enum 关键字前面加上 const 修饰,在编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,不会生成对应的对象,这样能提高性能表现。

const enum Color {
  Red,
  Green,
  Blue,
}

const x = Color.Red;
const y = Color.Green;
const z = Color.Blue;

// 编译后
const x = 0; /* Color.Red */
const y = 1; /* Color.Green */
const z = 2; /* Color.Blue */

同名 Enum 的合并

多个同名的 Enum 结构会自动合并。

Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。

enum Foo {
  A,
}

enum Foo {
  B, // 报错
}

不能有同名成员,否则报错。

enum Foo {
  A,
  B,
}

enum Foo {
  B = 1, // 报错
  C,
}

同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。

// 报错
enum E {
  A,
}
const enum E {
  B = 1,
}

字符串 Enum

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

字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。

enum Foo {
  A, // 0
  B = 'hello',
  C, // 报错
}

除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。

keyof 运算符

keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。

enum MyEnum {
  A = 'a',
  B = 'b',
}

// 'A'|'B'
type Foo = keyof typeof MyEnum;

// 返回 Enum 所有的成员值,可以使用in运算符
// { a: any, b: any }
type Foo = { [key in MyEnum]: any };

反向映射

数值 Enum 存在反向映射,即可以通过成员值获得成员名。

enum Weekdays {
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday,
}
console.log(Weekdays[3]); // Wednesday

8.类型断言

简介

type T = 'a' | 'b' | 'c';

let foo = 'a';
let bar: T = foo as T; // 正确

类型断言有两种语法。

// 语法一:<类型>值
<Type>value;

// 语法二:值 as 类型
value as Type;

// 语法一因为跟 JSX 语法冲突,现在一般都使用语法二

类型断言的条件

const n = 1;
const m: string = n as string; // 报错

类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件:

expr as T;
// expr 是 T 的子类型,或者 T 是 expr 的子类型

如果真的要断言成一个完全无关的类型,需要连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为 any 类型和 unknown 类型是所有其他类型的父类型。

// 或者写成 <T><unknown>expr
expr as unknown as T;

as const 断言

let s = 'JavaScript'; // string 类型

type Lang = 'JavaScript' | 'TypeScript' | 'Python'; // 联合类型

function setLang(language: Lang) {
  /* ... */
}

setLang(s); // 报错, string 类型(父类型)不能赋值给 Lang 类型(子类型)

报错的解决方法:

// 方法一:
const s = 'JavaScript';

// 方法二:
let s = 'JavaScript' as const;
// 使用了 as const 断言以后,let 变量就不能再改变值了

注意,as const 断言只能用于字面量,不能用于变量。

let s = 'JavaScript';
setLang(s as const); // 报错

// 另外,as const也不能用于表达式
let s = ('Java' + 'Script') as const; // 报错

as const 也可以写成前置的形式。

// 后置形式
expr as const

// 前置形式
<const>expr

as const 断言可以用于整个对象,也可以用于对象的单个属性

const v1 = {
  x: 1,
  y: 2,
}; // 类型是 { x: number; y: number; }

const v2 = {
  x: 1 as const,
  y: 2,
}; // 类型是 { x: 1; y: number; }

const v3 = {
  x: 1,
  y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }

// 数组
// a1 的类型推断为 number[]
const a1 = [1, 2, 3];

// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;

由于 as const 会将数组变成只读元组,所以很适合用于函数的 rest 参数

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

const nums = [1, 2];
const total = add(...nums); // 报错

// 修改后
const nums = [1, 2] as const;
const total = add(...nums); // 正确

Enum 成员也可以使用 as const 断言。

enum Foo {
  X,
  Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X

非空断言

对于那些可能为空的变量(即可能等于 undefined 或 null),可以使用 !符号,保证这些变量不会为空。

const root = document.getElementById('root'); // 可能为 null

// 报错
root.addEventListener('click', (e) => {
  /* ... */
});

// 修改后
const root = document.getElementById('root')!; // 不可能为 null

断言函数

function isString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw new Error('Not a string');
}

其中assertsis都是关键词,value是函数的参数名,string是函数参数的预期类型

9.模块

简介

TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。

// a.ts
export type Bool = true | false;

// 也可以写成两行
type Bool = true | false;
export { Bool };

// b.ts
// 再另一个文件中引入
import { Bool } from './a';
let foo: Bool = true;

import type 语句

// a.ts
export interface A {
  foo: string;
}

export let a = 123;

// b.ts
import { A, a } from './a'; // A 是类型,a 是正常接口

上面写法的问题是,不利于区分类型和正常接口。有两种方法解决:

// 方法一
import { type A, a } from './a';

// 方法二, import type 只能输出类型,不能输出正常接口
import type { A } from './a';

// 输入默认类型
import type DefaultType from 'moduleA';
// 输入所有类型
import type * as TypeNS from 'moduleA';

CommonJS 模块

使用import =语句和require()命令输入了一个 CommonJS 模块

import fs = require('fs');

// 或者
import * as fs from 'fs';

10.装饰器

简介

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。

装饰器有如下几个特征:

  • (1)第一个字符(或者说前缀)是@,后面是一个表达式。
  • (2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
  • (3)这个函数接受所修饰对象的一些相关值作为参数。
  • (4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
function simpleDecorator(value: any, context: any) {
  console.log(`hi, this is ${context.kind} ${context.name}`);
  return value;
}

@simpleDecorator
class A {} // "hi, this is class A"

函数simpleDecorator()用作装饰器,附加在类A之上,后者在代码解析时就会打印一行日志。

装饰器一般只用来为类添加某种特定行为。

@frozen
class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

上面示例中,一共有四个装饰器,一个用在类本身(@frozen),另外三个用在类的方法(@configurable@enumerable@throttle)。

装饰器的结构

装饰器函数的类型定义如下。

type Decorator = (
  value: DecoratedValue,
  context: {
    kind: string;
    name: string | symbol;
    addInitializer?(initializer: () => void): void;
    static?: boolean;
    private?: boolean;
    access: {
      get?(): unknown;
      set?(value: unknown): void;
    };
  }
) => void | ReplacementValue;

上面代码中,Decorator是装饰器的类型定义。它是一个函数,使用时会接收到valuecontext两个参数。

  • value:所装饰的对象。
  • context:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext,描述这个对象。

context对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kindname)是必有的,其他都是可选的。

(1)kind:字符串,表示所装饰对象的类型,可能取以下的值。

  • ‘class’
  • ‘method’
  • ‘getter’
  • ‘setter’
  • ‘field’
  • ‘accessor’

(2)name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。

(3)addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入 addInitializer()方法。注意,addInitializer()没有返回值。

(4)private:布尔值,表示所装饰的对象是否为类的私有成员。

(5)static:布尔值,表示所装饰的对象是否为类的静态成员。

(6)access:一个对象,包含了某个值的 get 和 set 方法。

类装饰器

类装饰器的类型描述如下。

type ClassDecorator = (
  value: Function,
  context: {
    kind: 'class';
    name: string | undefined;
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

方法装饰器

方法装饰器的类型描述如下。

type ClassMethodDecorator = (
  value: Function,
  context: {
    kind: 'method';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

属性装饰器

类型描述如下

type ClassFieldDecorator = (
  value: undefined,
  context: {
    kind: 'field';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown, set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
  }
) => (initialValue: unknown) => unknown | void;

getter 装饰器,setter 装饰器

类型描述如下

type ClassGetterDecorator = (
  value: Function,
  context: {
    kind: 'getter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

type ClassSetterDecorator = (
  value: Function,
  context: {
    kind: 'setter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

11.declare

简介

declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
declare 关键字可以描述以下类型。

  • 变量(const、let、var 命令声明)
  • type 或者 interface 命令声明的类型
  • class
  • enum
  • 函数(function)
  • 模块(module)
  • 命名空间(namespace)

declare variable

declare 关键字可以给出外部变量的类型描述。

其他脚本定义的全局变量x,使用 declare 命令给出它的类型。

declare let x:number;
x = 1; // 声明类型后使用就不会再报错

declare function

declare 关键字可以给出外部函数的类型描述。

declare function sayHello(name: string): void;

sayHello('张三');

declare class

declare class Animal {
  constructor(name: string);
  eat(): void;
  sleep(): void;
}

declare module,declare namespace

如果想把变量、函数、类组织在一起,可以将 declare 与 module 或 namespace 一起使用。

declare namespace AnimalLib {
  class Animal {
    constructor(name: string);
    eat(): void;
    sleep(): void;
  }

  type Animals = 'Fish' | 'Dog';
}

// 或者
declare module AnimalLib {
  class Animal {
    constructor(name: string);
    eat(): void;
    sleep(): void;
  }

  type Animals = 'Fish' | 'Dog';
}

declare module 和 declare namespace 里面,加不加 export 关键字都可以。

declare global

如果要为 JavaScript 引擎的原生对象添加属性和方法,可以使用declare global {}语法。

export {};

declare global {
  interface String {
    toSmallString(): string;
  }
}

String.prototype.toSmallString = (): string => {
  // 具体实现
  return '';
};

上面示例中,为 JavaScript 原生的String对象添加了toSmallString()方法。declare global 给出这个新增方法的类型描述。

这个示例第一行的空导出语句export {},作用是强制编译器将这个脚本当作模块处理。这是因为 declare global 必须用在模块里面。

declare enum

declare enum E1 {
  A,
  B,
}

declare enum E2 {
  A = 0,
  B = 1,
}

declare const enum E3 {
  A,
  B,
}

declare const enum E4 {
  A = 0,
  B = 1,
}

12. d.ts 类型声明文件

简介

单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。

文件名一般为[模块名].d.ts的形式,其中的d表示 declaration(声明)。

类型声明文件的来源

类型声明文件主要有以下三种来源。

  • TypeScript 编译器自动生成。
  • TypeScript 内置类型文件。
  • 外部模块的类型声明文件,需要自己安装。

1.自动生成

只要使用编译选项 declaration,编译器就会在编译时自动生成单独的类型声明文件。

下面是在tsconfig.json文件里面,打开这个选项。

{
  "compilerOptions": {
    "declaration": true
  }
}

2.内置声明文件

安装 TypeScript 语言时,会同时安装一些内置的类型声明文件,这些内置声明文件位于 TypeScript 语言安装目录的 lib 文件夹内。例如:

  • lib.d.ts
  • lib.dom.d.ts
  • lib.es2015.d.ts
  • lib.es2016.d.ts
  • lib.es2017.d.ts
  • lib.es2018.d.ts
  • lib.es2019.d.ts
  • lib.es2020.d.ts
  • lib.es5.d.ts
  • lib.es6.d.ts

TypeScript 编译器会自动根据编译目标 target 的值,加载对应的内置声明文件,所以不需要特别的配置。可以使用编译选项 lib,指定加载哪些内置声明文件。

{
  "compilerOptions": {
    "lib": ["dom", "es2021"]
  }
}

3.外部模块的类型声明文件

如果项目中使用了外部的某个第三方代码库,那么就需要这个库的类型声明文件。

分成三种情况。

(1)这个库自带了类型声明文件。

一般来说,如果这个库的源码包含了[vendor].d.ts文件,那么就自带了类型声明文件。

(2)这个库没有自带,但是可以找到社区制作的类型声明文件。

第三方库如果没有提供类型声明文件,社区往往会提供。如:"@types/jquery"

TypeScript 会自动加载node_modules/@types目录下的模块,但可以使用编译选项typeRoots改变这种行为。

{
  "compilerOptions": {
    "typeRoots": ["./typings", "./vendor/types"]
  }
}

上面示例表示,TypeScript 不再去node_modules/@types目录,而是去跟当前tsconfig.json同级的typingsvendor/types子目录,加载类型模块了。

默认情况下,TypeScript 会自动加载typeRoots目录里的所有模块,编译选项types可以指定加载哪些模块。

{
  "compilerOptions": {
    "types": ["jquery"]
  }
}

(3)找不到类型声明文件,需要自己写。

有时实在没有第三方库的类型声明文件,又很难完整给出该库的类型描述,这时你可以告诉 TypeScript 相关对象的类型是any

declare var $: any;

// 或者
declare type JQuery = any;
declare var $: JQuery;

模块发布

当前模块如果包含自己的类型声明文件,可以在 package.json 文件里面添加一个types字段或typings字段,指明类型声明文件的位置。

{
  "name": "awesome",
  "author": "Vandelay Industries",
  "version": "1.0.0",
  "main": "./lib/main.js",
  "types": "./lib/main.d.ts"
}

三斜杠命令

如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。

举例来说,入口文件是main.d.ts,里面的接口定义在interfaces.d.ts,函数定义在functions.d.ts。那么,main.d.ts里面可以用三斜杠命令,加载后面两个文件。

/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />

注意,三斜杠命令只能用在文件的头部,如果用在其他地方,会被当作普通的注释。

三斜杠命令主要包含三个参数,代表三种不同的命令。

  • path
  • types
  • lib

1./// <reference path="" />

告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。

/// <reference path="./lib.ts" />

let count = add(1, 2);

上面示例表示,当前脚本依赖于./lib.ts,里面是add()的定义。编译当前脚本时,还会同时编译./lib.ts。编译产物会有两个 JS 文件,一个当前脚本,另一个就是./lib.js

2./// <reference types="" />

types 参数用来告诉编译器当前脚本依赖某个 DefinitelyTyped 类型库,通常安装在node_modules/@types目录。

/// <reference types="node" />

上面示例中,这个三斜杠命令表示编译时添加 Node.js 的类型库,实际添加的脚本是 node_modules 目录里面的@types/node/index.d.ts

/// <reference lib="" />

允许脚本文件显式包含内置 lib 库,等同于在tsconfig.json文件里面使用 lib 属性指定 lib库。

13.TypeScript 类型运算符

keyof 运算符

keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。

type MyObj = {
  foo: number;
  bar: string;
};

type Keys = keyof MyObj; // 'foo'|'bar'

由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol

// string | number | symbol
type KeyT = keyof any;

对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。

type KeyT = keyof object; // never

keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。

type MyObj = {
  foo: number;
  bar: string;
};

type Keys = keyof MyObj;

type Values = MyObj[Keys]; // number|string

keyof 运算符往往用于精确表达对象的属性类型。

function prop<Obj, K extends keyof Obj>(obj: Obj, key: K): Obj[K] {
  return obj[key];
}

上面示例中,K extends keyof Obj表示KObj的一个属性名,传入其他字符串会报错。返回值类型Obj[K]就表示K这个属性值的类型。

keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。

type NewProps<Obj> = {
  [Prop in keyof Obj]: boolean;
};

// 用法
type MyObj = { foo: number };

// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;

下面的例子是让可选属性变成必有的属性。

type Concrete<Obj> = {
  [Prop in keyof Obj]-?: Obj[Prop];
};

// 用法
type MyObj = {
  foo?: number;
};

// 等于 { foo: number; }
type NewObj = Concrete<MyObj>;

上面示例中,[Prop in keyof Obj]后面的-?表示去除可选属性设置。对应地,还有+?的写法,表示添加可选属性设置。

in 运算符

JavaScript 语言中,in运算符用来确定对象是否包含某个属性名。

const obj = { a: 123 };

if ('a' in obj) console.log('found a');

TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。

type U = 'a' | 'b' | 'c';

type Foo = {
  [Prop in U]: number;
};
// 等同于
type Foo = {
  a: number;
  b: number;
  c: number;
};

方括号运算符

方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。

type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// Age 的类型是 number
type Age = Person['age'];

方括号的参数如果是联合类型,那么返回的也是联合类型。

type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// number|string
type T = Person['age' | 'name'];

// number|string|boolean
type A = Person[keyof Person];

方括号运算符的参数也可以是属性名的索引类型。

type Obj = {
  [key: string]: number;
};

// number
type T = Obj[string];

extends…?: 条件运算符

条件运算符extends...?:可以根据当前类型是否符合某种条件,返回不同的类型。

// 类型T是否可以赋值给类型U,即T是否为U的子类型
T extends U ? X : Y

// true
type T = 1 extends number ? true : false;

infer 关键字

infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。

is 运算符

函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

上面示例中,函数isFish()的返回值类型为pet is Fish,表示如果参数pet类型为Fish,则返回true,否则返回false

satisfies 运算符

satisfies运算符用来检测某个值是否符合指定类型。

14.类型映射

简介

映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。

type A = {
  foo: number;
  bar: number;
};

type B = {
  foo: string;
  bar: string;
};

上面示例中,这两个类型的属性结构是一样的,但是属性的类型不一样。如果属性数量多的话,逐个写起来就很麻烦。

使用类型映射,就可以从类型 A 得到类型 B。

type A = {
  foo: number;
  bar: number;
};

type B = {
  [prop in keyof A]: string;
};

具体的计算规则如下:

  • prop:属性名变量,名字可以随便起。
  • in:运算符,用来取出右侧的联合类型的每一个成员。
  • keyof A:返回类型 A 的每一个属性名,组成一个联合类型。

TypeScript 内置的工具类型Readonly<T>可以将所有属性改为只读属性,实现也是通过映射。

// 将 T 的所有属性改为只读属性
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

映射修饰符

映射会原样复制原始对象的可选属性和只读属性。

type A = {
  a?: string;
  readonly b: number;
};

type B = {
  [Prop in keyof A]: A[Prop];
};

// 等同于
type B = {
  a?: string;
  readonly b: number;
};

TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的?修饰符和readonly修饰符。

  • +修饰符:写成+?+readonly,为映射属性添加?修饰符或 readonly 修饰符。
  • 修饰符:写成-?-readonly,为映射属性移除?修饰符或 readonly 修饰符。
// 添加可选属性
type Optional<Type> = {
  [Prop in keyof Type]+?: Type[Prop];
};

// 移除可选属性
type Concrete<Type> = {
  [Prop in keyof Type]-?: Type[Prop];
};

// 添加 readonly
type CreateImmutable<Type> = {
  +readonly [Prop in keyof Type]: Type[Prop];
};

// 移除 readonly
type CreateMutable<Type> = {
  -readonly [Prop in keyof Type]: Type[Prop];
};

TypeScript 原生的工具类型Required<T>专门移除可选属性,就是使用-?修饰符实现的。

另外,+?修饰符可以简写成?+readonly修饰符可以简写成readonly

键名重映射

TypeScript 4.1 引入了键名重映射(key remapping),允许改变键名。

type A = {
  foo: number;
  bar: number;
};

type B = {
  [p in keyof A as `${p}ID`]: number;
};

// 等同于
type B = {
  fooID: number;
  barID: number;
};

属性过滤

键名重映射还可以过滤掉某些属性。下面的例子是只保留字符串属性。

type User = {
  name: string;
  age: number;
};

type Filter<T> = {
  [K in keyof T as T[K] extends string ? K : never]: string;
};

type FilteredUser = Filter<User>; // { name: string }

它的键名重映射as T[K] extends string ? K : never],使用了条件运算符。如果属性值T[K]的类型是字符串,那么属性名不变,否则属性名类型改为never,即这个属性名不存在。

15.类型工具

  1. Awaited<Type> ,用来取出 Promise 的返回值类型
  2. ConstructorParameters<Type> ,提取构造方法Type的参数类型,组成一个元组类型返回
  3. Exclude<UnionType,ExcludedMembers> ,用来从联合类型UnionType里面,删除某些类型ExcludedMembers,组成一个新的类型返回
  4. Extract<UnionType, Union> ,用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回
  5. InstanceType<Type>,提取构造函数的返回值的类型(即实例类型)
  6. NonNullable<Type> ,用来从联合类型Type删除null类型和undefined类型,组成一个新类型返回
  7. Omit<Type, Keys> ,用来从对象类型Type中,删除指定的属性Keys,组成一个新的对象类型返回
  8. OmitThisParameter<Type>,从函数类型中移除 this 参数
  9. Parameters<Type> ,从函数类型Type里面提取参数类型,组成一个元组返回
  10. Partial<Type> ,返回一个新类型,将参数类型Type的所有属性变为可选属性
  11. Pick<Type, Keys> ,返回一个新的对象类型,第一个参数Type是一个对象类型,第二个参数Keys是Type里面被选定的键名
  12. Readonly<Type>,返回一个新类型,将参数类型Type的所有属性变为只读属性
  13. Record<Keys, Type> ,返回一个对象类型,参数Keys用作键名,参数Type用作键值类型
  14. Required<Type> ,返回一个新类型,将参数类型Type的所有属性变为必选属性
  15. ReadonlyArray<Type>,用来生成一个只读数组类型,类型参数Type表示数组成员的类型
  16. ReturnType<Type> ,提取函数类型Type的返回值类型,作为一个新类型返回
  17. ThisParameterType<Type> ,提取函数类型中this参数的类型
  18. ThisType<Type> ,不返回类型,只用来跟其他类型组成交叉类型,用来提示 TypeScript 其他类型里面的this的类型
  19. 字符串类型工具
  • Uppercase<StringType>,将字符串类型的每个字符转为大写
  • Lowercase<StringType>,将字符串的每个字符转为小写
  • Capitalize<StringType>,将字符串的第一个字符转为大写
  • Uncapitalize<StringType>,将字符串的第一个字符转为小写

16.注释指令

// @ts-nocheck

// @ts-nocheck告诉编译器不对当前脚本进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。

// @ts-nocheck

const element = document.getElementById(123);

// @ts-check

如果一个 JavaScript 脚本顶部添加了// @ts-check,那么编译器将对该脚本进行类型检查,不论是否启用了checkJs编译选项。

// @ts-check
let isChecked = true;

console.log(isChceked); // 报错, 拼写错误

// @ts-ignore

// @ts-ignore告诉编译器不对下一行代码进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。

let x: number;

x = 0;

// @ts-ignore
x = false; // 不报错

// @ts-expect-error

// @ts-expect-error主要用在测试用例,当下一行有类型错误时,它会压制 TypeScript 的报错信息(即不显示报错信息),把错误留给代码自己处理。

JSDoc

TypeScript 直接处理 JS 文件时,如果无法推断出类型,会使用 JS 脚本里面的 JSDoc 注释。

JSDoc 基本要求:

  • JSDoc 注释必须以/**开始,其中星号(*)的数量必须为两个。若使用其他形式的多行注释,则 JSDoc 会忽略该条注释。
  • JSDoc 注释必须与它描述的代码处于相邻的位置,并且注释在上,代码在下。

下面是 JSDoc 的一个简单例子。

/**
 * @param {string} somebody
 */
function sayHello(somebody) {
  console.log('Hello ' + somebody);
}

TypeScript 编译器支持大部分的 JSDoc 声明,举例如下:

  • @typedef,创建自定义类型,等同于 TypeScript 里面的类型别名。
/**
 * @typedef {(number | string)} NumberLike
 */

// 等同于
type NumberLike = string | number;
  • @type,定义变量的类型。
/**
 * @type {string}
 */
let a;

// @type定义了变量a的类型为string

// 在@type命令中允许使用 TypeScript 类型及其语法。
/**@type {true | false} */
let a;

/** @type {number[]} */
let b;

/** @type {Array<number>} */
let c;
  • @param,用于定义函数参数的类型。
/**
 * @param {string}  x
 */
function foo(x) {}

// 如果是可选参数,需要将参数名放在方括号[]里面
/**
 * @param {string}  [x]
 */
function foo(x) {}

// 可以指定参数默认值
/**
 * @param {string} [x="bar"]
 */
function foo(x) {}
  • @return,@returns,两个命令作用相同,指定函数返回值的类型。
/**
 * @return {boolean}
 */
function foo() {
  return true;
}
  • @extends 和类型修饰符,@extends 命令用于定义继承的基类。
/**
 * @extends {Base}
 */
class Derived extends Base {}

// @public、@protected、@private分别指定类的公开成员、保护成员和私有成员。
// @readonly指定只读成员。
class Base {
  /**
   * @public
   * @readonly
   */
  x = 0;

  /**
   *  @protected
   */
  y = 0;
}

参考文档

面试题

1.TypeScript 动态设置对象属性为可选的方法

// 方法有多种,这里只介绍两种
// 方法一
type WithOptionalProp<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// 方法二
type WithOptionalProp<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
} & {
  [P in K]?: T[P];
};

// 测试
type A = WithOptionalProp<{ foo: number; bar: string }, 'foo'>;
const a: A = {
  bar: 'a',
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值