# 扩展阅读

这部分有很多内容都是变化中,作为扩展学习时食用。

# 索引类型

这里要讲的,可不是前面讲接口的时候讲的索引类型。在学习接口内容的时候,讲过可以指定索引的类型。而本小节讲的索引类型包含两个内容:索引类型查询索引访问操作符。

# 索引类型查询操作符

keyof操作符,连接一个类型,会返回一个由这个类型的所有属性名组成的联合类型。来看例子:

interface Info {
  name: string;
  age: number;
}
let infoProp: keyof Info;
infoProp = "name";
infoProp = "age";
infoProp = "no"; // error 不能将类型“"no"”分配给类型“"name" | "age"”

通过例子可以看到,这里的keyof Info其实相当于"name" | “age”

通过和泛型结合使用,TS 就可以检查使用了动态属性名的代码:

// 这里使用泛型,并且约束泛型变量K的类型是"keyof T",也就是类型T的所有字段名组成的联合类型
function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] { 
  // 指定getValue的返回值类型为T[K][],即类型为T的值的属性值组成的数组
  return names.map(n => obj[n]); 
}

const info = {
  name: "toimc",
  age: 18
};

let values: string[] = getValue(info, ["name"]);
// error 不能将类型“number[]”分配给类型“string[]”
values = getValue(info, ["age"]); 

# 索引访问操作符

索引访问操作符也就是[],其实和访问对象的某个属性值是一样的语法,但是在 TS 中它可以用来访问某个属性的类型:

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

type NameType = Info["name"];
let name: NameType = 123; // error 不能将类型“123”分配给类型“string”

再来看个例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
  return o[name]; // o[name] is of type T[K]
}

这个函数中,两个参数的类型分别为泛型 T 和 K,而函数的返回值类型为T[K],只要函数的返回值也是这种形式,即访问参数 o 的参数 name 属性,即可。

最后来看个结合接口的例子:

interface Obj<T> {
  [key: number]: T;
}

const key: keyof Obj<number>; // keys的类型为number

这里需要注意,如果索引类型为 number,那么实现该接口的对象的属性名必须是 number 类型;

但是如果接口的索引类型是 string 类型,那么实现该接口的对象的属性名设置为数值类型的值也是可以的,因为数值最后还是会先转换为字符串。

这里一样,如果接口的索引类型设置为 string 的话,keyof Obj<number>等同于类型number | string

interface Obj<T> {
  [key: string]: T;
}

let key: keyof Obj<number>; // keys的类型为number | string
key = 123; // right

也可以使用访问操作符,获取索引签名的类型:

interface Obj<T> {
  [key: string]: T;
}

const obj: Obj<number> = {
  age: 18
};

let value: Obj<number>["age"]; // value的类型是number,也就是name的属性值18的类型

还有一点,在讲后面知识的时候会遇到,就是当tsconfig.json里strictNullChecks设为false时,通过Type[keyof Type]获取到的,是除去never & undefined & null这三个类型之后的字段值类型组成的联合类型,来看例子:

interface Type {
  a: never;
  b: never;
  c: string;
  d: number;
  e: undefined;
  f: null;
  g: object;
}
type test = Type[keyof Type];
// test的类型是string | number | object

这个例子中接口 Type 有几个属性,通过索引访问操作符和索引类型查询操作符可以选出类型不为 never & undefined & null 的类型。

# 映射类型

TS 提供了借助旧类型创建一个新类型的方式,也就是映射类型,它可以用相同的形式去转换旧类型中每个属性。来看个例子:

interface Info {
  age: number;
}

可以使用这个接口实现一个有且仅有一个 age 属性的对象,但如果想再创建一个只读版本的同款对象,那可能需要再重新定义一个接口,然后让 age 属性 readonly。

如果接口就这么简单,确实可以这么做,但是如果属性多了,而且这个结构以后会变,那就比较麻烦了。

这种情况可以使用映射类型,下面来看例子:

interface Info {
  age: number;
}

type ReadonlyType<T> = { readonly [P in keyof T]: T[P] }; // 这里定义了一个ReadonlyType<T>映射类型

type ReadonlyInfo = ReadonlyType<Info>;

let info: ReadonlyInfo = {
  age: 18
};

info.age = 28; // error Cannot assign to 'age' because it is a constant or a read-only property

这个例子展示了如何通过一个普通的接口创建一个每个属性都只读的接口,这个过程有点像定义了一个函数,这个函数会遍历传入对象的每个属性并做处理。

注意了,在这里用到了一个新的操作符 in,TS 内部使用了 for … in,定义映射类型,这里涉及到三个部分:

  • 类型变量,也就是上例中的 P,它就像 for…in 循环中定义的变量,用来在每次遍历中绑定当前遍历到的属性名;
  • 属性名联合,也就是上例中keyof T,它返回对象 T 的属性名联合;
  • 属性的结果类型,也就是 T[P]。

因为这两个需求较为常用,所以 TS 内置了这两种映射类型,无需定义即可使用,它们分别是ReadonlyPartial

还有两个内置的映射类型分别是PickRecord,它们的实现如下:

type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };

先来使用一下 Pick,官方文档的例子并不完整,来看完整的例子:

interface Info {
  name: string;
  age: number;
  address: string;
}

const info: Info = {
  name: "toimc",
  age: 18,
  address: "beijing"
};

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { // 这里定义一个pick函数,用来返回一个对象中指定字段的值组成的对象
  let res = {} as Pick<T, K>;
  keys.forEach(key => {
    res[key] = obj[key];
  });
  return res;
}

const nameAndAddress = pick(info, ["name", "address"]); // { name: 'toimc', address: 'beijing' }

另外一个就是 Record,它适用于将一个对象中的每一个属性转换为其他值的场景,来看例子:

function mapObject<K extends string | number, T, U>(
  obj: Record<K, T>,
  f: (x: T) => U
): Record<K, U> {
  let res = {} as Record<K, U>;
  for (const key in obj) {
    res[key] = f(obj[key]);
  }
  return res;
}

const names = { 0: "hello", 1: "world", 2: "bye" };
const lengths = mapObject(names, s => s.length); // { 0: 5, 1: 5, 2: 3 }

输入的对象属性值为字符串类型,输出的对象属性值为数值类型。

这四个内置映射类型中,Readonly、Partial 和 Pick 是同态的,而 Record 不是,因为 Record 映射出的对象属性值是新的,和输入的值的属性值不同。

同态:两个相同类型的代数结构之间的结构保持映射

# 由映射类型进行推断

学习了使用映射类型包装一个类型的属性后,也可以进行逆向操作,也就是拆包,先来看的包装操作:

type Proxy<T> = { // 这里定义一个映射类型,他将一个属性拆分成get/set方法
  get(): T;
  set(value: T): void;
};
type Proxify<T> = { [P in keyof T]: Proxy<T[P]> }; // 这里再定义一个映射类型,将一个对象的所有属性值类型都变为Proxy<T>处理之后的类型
function proxify<T>(obj: T): Proxify<T> { // 这里定义一个proxify函数,用来将对象中所有属性的属性值改为一个包含get和set方法的对象
  let result = {} as Proxify<T>;
  for (const key in obj) {
    result[key] = {
      get: () => obj[key],
      set: value => (obj[key] = value)
    };
  }
  return result;
}

let props = {
  name: "toimc",
  age: 18
};

let proxyProps = proxify(props);
console.log(proxyProps.name.get()); // "toimc"
proxyProps.name.set("li");

来看下这个例子,这个例子定义了一个函数,这个函数可以把传入的对象的每个属性的值替换为一个包含 get 和 set 两个方法的对象。最后获取某个值的时候,比如 name,就使用 proxyProps.name.get()方法获取它的值,使用 proxyProps.name.set()方法修改 name 的值。

接下来来看如何进行拆包:

function unproxify<T>(t: Proxify<T>): T { // 这里定义一个拆包函数,其实就是利用每个属性的get方法获取到当前属性值,然后将原本是包含get和set方法的对象改为这个属性值
  let result = {} as T;
  for (const k in t) {
    result[k] = t[k].get(); // 这里通过调用属性值这个对象的get方法获取到属性值,然后赋给这个属性,替换掉这个对象
  }
  return result;
}
let originalProps = unproxify(proxyProps);

# 增加或移除特定修饰符

TS 在 2.8 版本为映射类型增加了增加或移除特定修饰符的能力,使用+-符号作为前缀来指定增加还是删除修饰符。

首先来看如何通过映射类型为一个接口的每个属性增加修饰符,这里使用+前缀:

interface Info {
  name: string;
  age: number;
}
// 通过+前缀增加了 readonly 和?修饰符, 当然,增加的时候,这个+前缀可以省略
// 等价 type ReadonlyInfo = { readonly [P in keyof T]?: T[P] }
type ReadonlyInfo<T> = { +readonly [P in keyof T]+?: T[P] };
let info: ReadonlyInfo<Info> = {
  name: "toimc"
};
info.name = ""; // error

这个例子中,经过 ReadonlyInfo 创建的接口类型,属性是可选的,所以在定义 info 的时候没有写 age 属性也没问题。

同时每个属性是只读的,所以修改 name 的值的时候报错。

再来看下怎么删除修饰符:

interface Info {
  name: string;
  age: number;
}
type RemoveModifier<T> = { -readonly [P in keyof T]-?: T[p] };
type InfoType = RemoveModifier<Readonly<Partial<Info>>>;
let info1: InfoType = {
  // error missing "age"
  name: "toimc"
};
let info2: InfoType = {
  name: "toimc",
  age: 18
};
info2.name = ""; // right, can edit

这个例子定义了去掉修饰符的映射类型 RemoveModifier,Readonly<Partial<Info>>则是返回一个既属性可选又只读的接口类型,所以 InfoType 类型则表示属性必含而且非只读。

TS 内置了一个映射类型Required<T>,使用它可以去掉 T 所有属性的?修饰符。

# keyof 和映射类型在 2.9 的升级

TS 在 2.9 版本中,keyof 和映射类型支持用 number 和 symbol 命名的属性,先来看 keyof 的例子:

const stringIndex = "a";
const numberIndex = 1;
const symbolIndex = Symbol();
type Obj = {
  [stringIndex]: string;
  [numberIndex]: number;
  [symbolIndex]: symbol;
};
type keys = keyof Obj;
let key: keys = 2; // error
let key: keys = 1; // right
let key: keys = "b"; // error
let key: keys = "a"; // right
let key: keys = Symbol(); // error
let key: keys = symbolIndex; // right

再来看个映射类型的例子:

const stringIndex = "a";
const numberIndex = 1;
const symbolIndex = Symbol();
type Obj = {
  [stringIndex]: string;
  [numberIndex]: number;
  [symbolIndex]: symbol;
};
type ReadonlyType<T> = { readonly [P in keyof T]?: T[P] };
let obj: ReadonlyType<Obj> = {
  a: "aa",
  1: 11,
  [symbolIndex]: Symbol()
};
obj.a = "bb"; // error Cannot assign to 'a' because it is a read-only property
obj[1] = 22; // error Cannot assign to '1' because it is a read-only property
obj[symbolIndex] = Symbol(); // error Cannot assign to '[symbolIndex]' because it is a read-only property

# 元组和数组上的映射类型

TS 在 3.1 版本中,在元组和数组上的映射类型会生成新的元组和数组,并不会创建一个新的类型,这个类型上会具有 push、pop 等数组方法和数组属性。来看例子:

type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };
type Tuple = [number, string, boolean];
type promiseTuple = MapToPromise<Tuple>;
let tuple: promiseTuple = [
  new Promise((resolve, reject) => resolve(1)),
  new Promise((resolve, reject) => resolve("a")),
  new Promise((resolve, reject) => resolve(false))
];

这个例子中定义了一个MapToPromise映射类型。

它返回一个将传入的类型的所有字段的值转为Promise,且Promise的resolve回调函数的参数类型为这个字段类型。

定义了一个元组Tuple,元素类型分别为number、string和boolean,使用MapToPromise映射类型将这个元组类型传入,并且返回一个promiseTuple类型。

当指定变量tuple的类型为promiseTuple后,它的三个元素类型都是一个Promise,且resolve的参数类型依次为number、string和boolean。

# 条件类型

# 基础使用

条件类型是 TS2.8 引入的,从语法上看它像是三元操作符。它会以一个条件表达式进行类型关系检测,然后在后面两种类型中选择一个,先来看它怎么写:

T extends U ? X : Y

这个表达式的意思是,如果 T 可以赋值给 U 类型,则是 X 类型,否则是 Y 类型。来看个实际例子:

type Type<T> = T extends string | number
let index: Type<'a'> // index的类型为string
let index2: Type<false> // index2的类型为number

# 分布式条件类型

当待检测的类型是联合类型,则该条件类型被称为“分布式条件类型”,在实例化时会自动分发成联合类型,来看例子:

type TypeName<T> = T extends any ? T : never;
type Type1 = TypeName<string | number>; // Type1的类型是string|number

可能会说,既然想指定 Type1 的类型为 string|number,为什么不直接指定,而要使用条件类型?

其实这只是简单的示范,条件类型可以增加灵活性,再来看个复杂点的例子,这是官方文档的例子:

type TypeName<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : T extends undefined
  ? undefined
  : T extends Function
  ? Function
  : object;
type Type1 = TypeName<() => void>; // Type1的类型是Function
type Type2 = TypeName<string[]>; // Type2的类型是object
type Type3 = TypeName<(() => void) | string[]>; // Type3的类型是object | Function

来看一个分布式条件类型的实际应用:

type Diff<T, U> = T extends U ? never : T;
type Test = Diff<string | number | boolean, undefined | number>;
// Test的类型为string | boolean

这个例子定义的条件类型的作用就是,找出从 T 中出去 U 中存在的类型,得到剩下的类型。不过这个条件类型已经内置在 TS 中了,只不过它不叫 Diff,叫 Exclude,待会儿会讲到。

来看一个条件类型和映射类型结合的例子:

type Type<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
interface Part {
  id: number;
  name: string;
  subparts: Part[];
  updatePart(newName: string): void;
}
type Test = Type<Part>; // Test的类型为"updatePart"

来看一下,这个例子中,接口 Part 有四个字段,其中 updatePart 的值是函数,也就是 Function 类型。

Type的定义中,涉及到映射类型、条件类型、索引访问类型和索引类型。

首先[K in keyof T]用于遍历 T 的所有属性名,值使用了条件类型,T[K]是当前属性名的属性值,T[K] extends Function ? K : never表示如果属性值为 Function 类型,则值为属性名字面量类型,否则为 never 类型。

接下来使用keyof T获取 T 的属性名,最后通过索引访问类型[keyof T]获取不为 never 的类型。

# 条件类型的类型推断

条件类型提供一个infer关键字用来推断类型,先来看个例子。

想定义一个条件类型,如果传入的类型是一个数组,则返回它元素的类型;如果是一个普通类型,则直接返回这个类型。来

看下不使用 infer 的话,怎么写:

type Type<T> = T extends any[] ? T[number] : T;
type test = Type<string[]>; // test的类型为string
type test2 = Type<string>; // test2的类型为string

这个例子中,如果传入 Type 的是一个数组类型,那么返回的类型为T[number],也就是该数组的元素类型。如果不是数组,则直接返回这个类型。

这里是自己通过索引访问类型T[number]来获取类型的,如果使用 infer 关键字则无需自己手动获取,来看下怎么使用 infer:

type Type<T> = T extends Array<infer U> ? U : T;
type test = Type<string[]>; // test的类型为string
type test2 = Type<string>; // test2的类型为string

这里 infer 能够推断出 U 的类型,并且供后面使用,可以理解为这里定义了一个变量 U 来接收数组元素的类型。

# 预定义条件类型

TS 在 2.8 版本增加了一些预定义的有条件类型,来看一下:

  • Exclude<T, U>,从 T 中去掉可以赋值给 U 的类型:
type Type = Exclude<"a" | "b" | "c", "a" | "b">;
// Type => 'c'
type Type2 = Exclude<string | number | boolean, string | number>;
// Type2 => boolean
  • Extract<T, U>,选取 T 中可以赋值给 U 的类型:
type Type = Extract<"a" | "b" | "c", "a" | "c" | "f">;
// Type => 'a' | 'c'
type Type2 = Extract<number | string | boolean, string | boolean>;
// Type2 => string | boolean
  • NonNullable,从 T 中去掉 null 和 undefined:
type Type = Extract<string | number | undefined | null>;
// Type => string | number
  • ReturnType,获取函数类型返回值类型:
type Type = ReturnType<() => string)>
// Type => string
type Type2 = ReturnType<(arg: number) => void)>
// Type2 => void
  • InstanceType,获取构造函数类型的实例类型:

InstanceType直接看例子可能不好理解,所以先来看下它的实现:

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

InstanceType 条件类型要求泛型变量 T 类型是创建实例为 any 类型的构造函数,而它本身则通过判断 T 是否是构造函数类型来确定返回的类型。如果是构造函数,使用 infer 可以自动推断出 R 的类型,即实例类型;否则返回的是 any 类型。

看过 InstanceType 的实现后,来看怎么使用:

class A {
  constructor() {}
}
type T1 = InstanceType<typeof A>; // T1的类型为A
type T2 = InstanceType<any>; // T2的类型为any
type T3 = InstanceType<never>; // T3的类型为never
type T4 = InstanceType<string>; // error

上面例子中:

T1 的定义中,typeof A返回的的是类 A 的类型,也就是 A,这里不能使用 A 因为它是值不是类型,类型 A 是构造函数,所以 T1 是 A 构造函数的实例类型,也就是 A;T2 传入的类型为 any,因为 any 是任何类型的子类型,所以它满足T extends new (…args: any[]) => infer R,这里 infer 推断的 R 为 any;传入 never 和 any 同理。传入 string 时因为 string 不能不给构造函数类型,所以报错。

# 混入

混入即把两个对象或者类的内容,混合起来,从而实现一些功能的复用。

如果使用过 Vue,应该知道 Vue 的 mixins 这个 api,它可以允许将一些抽离到对象的属性、方法混入到一些组件。接下来先来看看个在 JavaScript 中实现的简单混入:

class A {
  constructor() {}
  funcA() {
    console.log("here");
  }
}
class B {
  constructor() {}
  funcB() {}
}

const mixin = (target, from) => { // 这里定义一个函数来将一个类混入到目标类
  Object.getOwnPropertyNames(from).forEach(key => { // 通过Object.getOwnPropertyNames可以获取一个对象自身定义的而非继承来的属性名组成的数组
    target[key] = from[key]; // 将源类原型对象上的属性拿来添加到目标类的原型对象上
  });
};

mixin(B.prototype, A.prototype); // 传入两个类的原型对象
const b = new B();
b.funcA(); // here

通过Object.getOwnPropertyNames方法获取一个对象自身的属性,这里自身指除去继承的属性,获取到属性后将属性赋值给目标对象。

这是 JavaScript 中的简单混入,在 TypeScript 中知道,除了值还有类型的概念,所以简单地将属性赋值到目标元素是不行的,还要处理类型定义,来看下 TypeScript 中混入的例子:

class ClassAa {
  isA: boolean;
  funcA() {}
}
class ClassBb {
  isB: boolean;
  funcB() {}
}
// 定义一个类类型接口AB,在讲类的时候补充过类和接口之间的继承,也讲过类类型接口
// 这里是让类AB继承ClassAa和ClassBb的类型,所以使用implements关键字,而不是用extends
class AB implements ClassAa, ClassBb {        
  constructor() {}
  isA: boolean = false; // 定义两个实例属性
  isB: boolean = false;
  funcA: () => void; // 定义两个方法,并指定类型
  funcB: () => void;
}

function mixins(base: any, from: any[]) { // 这里改造一下,直接传入类,而非其原型对象,base是最后要汇总而成的类,from是个数组,是要混入的源类组成的数组
  from.forEach(fromItem => {
    Object.getOwnPropertyNames(fromItem.prototype).forEach(key => {
      base.prototype[key] = fromItem.prototype[key];
    });
  });
}

mixins(AB, [ClassAa, ClassBb]);
const ab = new AB();
console.log(ab);
/*
{
    isA: false,
    isB: false,
    __proto__: {
        funcA: f ()
        funcB: f ()
        constructor: f
    }
}
*/

这个例子中定义了两个类 A 和 B,它们分别有自己的方法和实力属性。

如果想使用它们的所有属性和方法来创建实例,就需要将它们做一个混合。因为包含类型定义,所以首先要定义一个接口,来包含着两个类中元素类型的定义。

定义一个类类型接口,然后让这个类类型接口 AB 通过 implements 继承 A 和 B 这两个类,这样类 AB 就会同时拥有类 A 和 B 的类型定义,还有自身定义的一些类型和值。

所以此时类 AB 相当于:

class AB {
  isA: boolean = false;
  isB: boolean = false;
  funcA: () => void;
  funcB: () => void;
}

通过 mixins 函数将类 A 和类 B 的原型对象的属性方法赋值给类 AB,因为类 AB 有 funcA 和 funcB 的类型定义,可以把 funcA 和 funcB 函数实体赋值给类 AB。

# Promise及async/await

与ES6+中

TS 在 1.6 版本实验性地支持了 async 函数。

在过去的 JavaScript 当中,如果想保证代码的执行顺序,需要使用回调函数,当需要执行的步骤多了时就会陷入当说的“回调地狱”。

自从 ES6 增加了 Promise 之后,状况有了缓解,先来看个例子,一个简单的多个 ajax 请求的例子:

ajax.post( // 这里可以先忽略ajax的定义,他的post方法用来发送一个post请求
  "/login", // 第一个参数时要请求的url
  {
    data: {
      user_name: "toimc",
      password: "xxxxx"
    }
  }, // 第二个参数是这个请求要携带的参数
  function(res) {
    var user_id = res.data.user_id;
    ajax.post( // 这里在/login接口成功返回数据后,再调用一个/user_roles接口,用来获取该登录用户的角色信息
      "/user_roles",
      {
        data: {
          user_id: user_id
        }
      },
      function(res) {
        var role = res.data.role;
        console.log(role);
      }
    );
  } // 第三个参数是接口响应之后的回调函数
);

在这个例子中:

  • 先调用登录的接口发送用户名和密码
  • 服务端进行校验之后返回这个用户的一些信息
  • 可以从信息中拿到用户 id 去获取它的角色用于权限控制。

这个过程是有先后顺序的,必须先登录后获取角色,为了保证这个顺序,在过去要使用回调函数,当然一些库也支持链式调用。

再来看下使用 ES6 的 Promise 需要怎么写:

const loginReq = ({ user_name, password }) => { // 封装一个loginReq函数,用来返回一个Promise,用来调用/login接口
  return new Promise((resolve, reject) => { // Promise接收一个回调函数参数,这个函数有两个参数,两个参数都是回调函数
    ajax.post(
      "/login",
      {
        user_name,
        password
      },
      res => {
        resolve(res); // 第一个参数resolve用来在执行成功后调用,传给他的参数,可以在这个promise的then函数参数中获取到
      },
      error => {
        reject(error); // 第二个参数reject用来在执行出现错误后调用,传给他的错误信息,可以在这个promise的catch函数参数中获取到
      }
    );
  });
};
const getRolesReq = ({ user_id }) => { // 封装一个getRolesReq函数,用来返回一个Promise,用来调用/user_roles接口
  return new Promise((resolve, reject) => {
    ajax.post(
      "/user_roles",
      {
        data: {
          user_id
        }
      },
      res => {
        resolve(res);
      },
      error => {
        reject(error);
      }
    );
  });
};
loginReq({ user_name: "toimc", password: "xxxxx" }).then(res => { // 这里在调用loginReq函数后返回一个Promise,在内部当执行到resolve的地方时,这里的then的回调函数就会执行
  getRolesReq({ user_id: res.data.user_id }).then(res => {
    console.log(res.data.role);
  });
});

这看起来代码变长了,但是当搭配使用诸如 Axios 这类 ajax 请求库和 ES6 语法时,对于一些复用性高的接口调用能够起到很好的封装作用,而且使用起来较为简洁。

ES7 中增加了 async 和 await 的规范,它们其实是 Promise 的语法糖。

TypeScript 在 1.6 支持了 async 和 await,下面通过 setTimeout 来实现异步过程,看下在 TypeScript 中如何使用 async 和 await:

interface Res { // 定义一个接口,用来定义接口返回结果的结构
  data: {
    [key: string]: any;
  };
}

// 首先定义一个命名空间 axios,定义它用来简单模拟 axios 这个 ajax 请求库。
namespace axios { // 现在来定义一个命名空间,用来模拟axios实现接口调用
  export function post(url: string, config: object): Promise<Res> { // 返回值类型是一个Promise,resolve传的参数的类型是Res
    return new Promise((resolve, reject) => { // 然后这里返回一个Promise
      setTimeout(() => { // 通过setTimeout实现异步效果
        let res: Res = { data: {} };
        if (url === "/login") res.data.user_id = 111; // 这里通过简单判断,来模拟调用不同接口返回不同数据的效果
        else res.data.role = "admin";
        console.log(2);
        resolve(res); // 在这里传入res结果
      }, 1000);
    });
  }
}
  
interface Info {
  user_name: string;
  password: string;
}
  
async function loginReq({ user_name, password }: Info) { // 这里使用async关键字修饰这个函数,那么他内部就可以包含异步逻辑了
  try {
    console.log(1);
    const res = await axios.post("/login", { // 这里调用/login接口
      data: {
        user_name,
        password
      }
    });
    console.log(3);
    return res;
  } catch (error) {
    throw new Error(error);
  }
}
  
async function getRoleReq(user_id: number) {
  try {
    const res = await axios.post("/user_roles", {
      data: {
        user_id
      }
    });
    return res;
  } catch (error) {
    throw new Error(error);
  }
}
  
loginReq({ user_name: "toimc", password: "123" }).then(res => {
  const {
    data: { user_id }
  } = res;
  getRoleReq(user_id).then(res => {
    const {
      data: { role }
    } = res;
    console.log(role);
  });
});