TS从入门到放弃【十三】:高级类型(二)

239 阅读6分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

1、this 类型

js 中,this 可以用来获取 全局对象、类实例对象、构建函数实例等引用。

ts 中,this 也是一种类型,我们先来看一个计算器的例子:

class Counter {
  constructor(public count: number = 0) {}
  public add(value: number) {
    this.count += value;
    return this;
  }
  public subtract(value: number) {
    this.count -= value;
    return this;
  }
}
let counter = new Counter(10);
counter.add(3).add(3).subtract(1); // 15

在上述例子中,addsubtract 这两个方法返回的都是 this(实例对象),此类方法可以进行链式调用。

接下来,我们再来定义一个 PowCounter 类,继承 Counter

class PowCounter extends Counter {
  constructor(public count: number = 0) {
    super(count);
  }
  // 幂运算
  public pow(value: number) {
    this.count = this.count ** value;
    return this;
  }
}
const powCounter = new PowCounter(2);
powCounter.pow(3).subtract(1); // 2^3 - 1 = 8 - 1 = 7

2、索引类型

索引类型查询、索引访问

(1)索引类型查询:keyof

keyof 连接一个类型,然后返回一个由这个类型所有属性名组成的联合类型

示例1:

interface InfoInterface {
  name: string;
  age: number;
}
let info: keyof InfoInterface; // 此时,info被赋值的内容就只能是 "name" 或者 "age" 了
info = "age"; // OK
info = "age1"; // Error:类型“"age1"”不可分配给类型“keyof InfoInterface”。

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

示例2:

function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] {
  // n代表每一个属性名,返回每一个属性值
  return names.map(n => obj[n]);
}
const info = {
  name: 'dylan',
  age: 18,
  sex: 'man'
}
let values = getValue(info, ['name', 'age']);
console.log(values); // ['dylan', 18]:返回“name、age”属性值组成的数组
  • K extends keyof T:K 代表 T 的全部属性名组成的联合类型。
  • 参数中,obj 的类型是 T,names 的类型是 K 组成的数组。
  • 返回值:T[K] 表示 T 的值,所以返回值类型是 T 的值组成的数组

(2)索引访问操作符: [ ]

其实和我们访问某个属性值是一样的,但是在 TS 中它可以访问某个属性的类型。

interface InfoInterface {
  name: string;
  age: number;
}
// 指定 NameTypes 的类型是 InfoInterface 中的 name 字段
type NameTypes = InfoInterface["name"]; // string:相当于是 string 类型

我们通过索引访问操作符 [] 访问到 name 字段(InfoInterface["name"]),它所访问到的类型就是 string 类型,所以 NameTypes 就是 string 类型。

我们再来看一个例子:

interface Objs<T> {
  [key: number]: T; // 索引类型为number,值的类型为T
}
let keys: keyof Objs<number>; // 此时 keys 的类型为 number

但如果 key 的类型是 string 类型的话,实现接口的属性类型可以是 string | number 类型,因为这里数值类型会被转换为字符串类型

interface Objs<T> {
  [key: string]: T;
}
let keys: keyof Objs<number>; // let keys: string | number
let objs: Objs<number> = {
  age: 18 // 因为Objs<number>中传入的是 number 类型,此类型代表 Objs 中的 T,所以 age 的值需要是 number 类型。
}

keyof 会返回类型不为 never、undefined、null 的属性名

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

3、映射类型

TS 中提供了借助旧类型创建新类型的方式,也就是映射类型。它能够以相同的方式,来转换旧类型中的每一个属性。

示例:

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

现在我们想定义一个新的类型,让 Info 中的参数都是只读的。

type ReadonlyType<T> = {
  readonly [P in keyof T]: T[P]
};
type ReadonlyInfo = ReadonlyType<Info>;
// 鼠标放上去ReadonlyInfo,可以看到如下代码提示
//	type ReadonlyInfo = {
//    readonly age: number;
//    readonly name: string;
//    readonly sex: string;
//	}
  • keyof T:由 T 中的所有参数名组成的数组
  • P in keyof T:P 就代表 T的参数名数组 中的每一个参数名
  • T[P]:代表参数值

TS 在内部实现 in 时,实际上使用的是 for...in 迭代器。

同理,我们来实现可选属性

type SelectType<T> = {
  [P in keyof T]?: T[P]
};
type SelectInfo = SelectType<Info>;
// 鼠标放上去 SelectInfo,可以看到如下代码提示
//	type SelectInfo = {
//    age?: number | undefined;
//    name?: string | undefined;
//    sex?: string | undefined;
//	}

4、内置的映射类型

上面的只读、可选类型映射,TS中有内置的映射类型:ReadonlyPartial

interface Info {
  age: number;
  name: string;
  sex: string;
}
type ReadonlyInfo = Readonly<Info>;
type SelectInfo = Partial<Info>;

(1)Pick

原来对象上的一部分属性名,组成的类名

// 源码实现
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

使用的时候,参数1为对象,参数2为这个对象的部分属性列表。返回的结果是这个对象的部分属性

interface Info {
  name: string;
  age: number;
  sex: string;
}
const info: Info = {
  name: 'dylan',
  age: 18,
  sex: 'man'
}
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const res: any = {};
  keys.map(key => {
    res[key] = obj[key];
  });
  return res;
}
const nameAndAge = pick(info, ['name', 'age']); 
console.log(nameAndAge); // // { name: 'dylan', age: 18 }
  • 泛型类型 T、K:K继承T的参数名(key)
  • 参数1(obj):类型T
  • 参数2(keys):K (T的参数名),组成的数组。K[]
  • Pick<T, K>:返回原先 T 上的部分参数

(2)Record

将对象中的每一个属性转换为其他值

// 源码实现
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

实际使用:

function mapObject<K extends string | number, T, U>(obj: Record<K, T>, fn: (x: T) => U): Record<K, U> {
  const res: any = {};
  for(const key in obj) {
    res[key] = fn(obj[key]);
  }
  return res;
}
  • 传入三个泛型:K(继承 string | number)、T、U
  • 参数1(obj):Record<K, T>
    • obj 这个对象中的 key 类型 需要从 K 中取,也就是 string | number
  • 参数2:回调函数 fn

我们来调用一下上面的函数:

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

最后的返回值类型(Record<K, U>),代表返回一个对象,对象的属性名需要是 names 的属性名中的一个,对象的 属性值 需要是回调函数 fn 的返回值类型。

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

刚刚我们所讲的这四个映射类型中,ReadonlyPartialPick 是同态的。

Record 则不是同态的,因为 Record 映射出的属性值是一个新的,和输入值的属性值是不同的。

5、拆包

我们先来看一下包装的操作:

type Proxy<T> = {
  get(): T;
  set(value: T): void;
};
type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>
};

Proxify 的属性名是传入对象的属性名,属性值是包裹了一层 Proxy传入对象属性值

现在我们来定义一个函数:

function proxify<T>(obj: T): Proxify<T> {
  const result = {} as Proxify<T>;
  for(const key in obj) {
    result[key] = {
      get: () => obj[key],
      set: (value) => obj[key] = value,
    };
  }
  return result;
}
  • 传入一个对象,返回值是被 Proxify 处理过的对象
  • result 的每一个属性值都变成了一个对象

定义完函数之后,我们来定义一个对象:

let props = {
  name: 'dylan',
  age: 18
};
let proxyProps = proxify(props);
console.log(proxyProps); // { name: get set, age: get set }
console.log(proxyProps.name.get()); // dylan

这时可以看到,proxyProps 上面的每一个属性都变成了一个对象,每个对象上面都有 get 和 set 方法。

接下来,我们基于上述代码,来进行拆包

function unproxify<T>(t: Proxify<T>): T {
  const result = {} as T;
  for(const k in t) {
    result[k] = t[k].get();
  }
  return result;
}
console.log(unproxify(proxyProps)); // {name: 'dylan', age: 18}