前言

使用ts也有一段时间了,但是一直在入门阶段不得要领,看着那些热门仓库的类型定义花里胡哨的,想着自己啥时候也能这么强啊,但是日常项目开发,顶多就是定义下type、interface,用一用泛型封装一下,对于一些特殊需求,不知道该如何下手。

比如:

一些第三方插件它虽然提供了类型声明,但是可能个别声明没有导出,如果你想使用这个类型,该怎么办呢?

我们在定义类型的时候,如果存在多个类型嵌套组合成一个大类型声明,一定要先声明小的类型,然后组合成大的类型吗?这种方式会导致大的类型在预览定义时会十分痛苦,因为只能看到小类型声明的名称,看不到具体定义的内容。

这几天逛b站发现了一个宝藏视频,介绍了常用的12中工具类型,然后我再结合平时的使用,写个笔记文档,方便以后查询使用。

bilibili视频:《TypeScript 中最常用的 12 种工具类型》

有兴趣看视频的可以看一下这个视频。


typeof 获取变量或表达式类型

有时候我们可能写了一个很大的对象,这个对象又是一个可以从字面量意义上明确知道类型的,比如我们在vuex中定义的state对象,我们没必要再单独写一个类型声明,我们可以通过typeof的方式让ts自动生成。

不过有一个小缺陷就是这样的类型它不会有/** */的注释,因为是自动生成的,哪怕你源对象有写这个注释,不过问题不大,使用的时候通过源对象调用的属性还是可以抓到注释的。

const a = {
  /** 姓名 */
  name: "a",
  /** 年龄 */
  age: 1,
  /** 性别 */
  test: "",
};

type A = typeof a;

除了变量,typeof也可用于表达式,比如函数,三元等。

Parameters 获取函数的参数类型

Parameters 本身就是参数的意思,它通过一个泛型参数的方式,将函数的类型传入,返回函数的参数类型,值是一个元组(Tuple)。

在ts中是有元组和数组之分的,元组的参数是固定的,而数组的不是,在强类型语言中元组是更加严格和精确的类型定义,可以提高代码的可读性和可维护性,还可以在一定程度上提高代码的性能。因此,在一些需要固定长度和类型的数组场景下,使用Tuple类型会更加合适。

元组不是什么神奇的玩意,它其实就是不省事的那种声明方式:

/** 元组类型 */
type ArrTuple = [string, string];

const arr: ArrTuple = ["1", "2"];

好了,课外知识结束,我们看看如何获取函数参数类型:

function test(name: string, age: number, ...args: Array<any>): string {
  return `my name is ${name}, age is ${age}, args is ${args}`;
}

/** 获取函数参数类型 */
type TestFnParams = Parameters<typeof test>;

使用还是很方便的。

ReturnType 获取函数返回值类型

function test(name: string, age: number, ...args: Array<any>): string {
  return `my name is ${name}, age is ${age}, args is ${args}`;
}

/** 获取函数返回值类型 */
type TestReturnType = ReturnType<typeof test>;

Awaited 获取Promise结果类型

这个还挺有用的,比如我们可以用这个去获取axios这些结果类型。

function test(): Promise<string> {
  return Promise.resolve("hello world");
}

/** 获取函数返回值类型 */
type TestReturnType = ReturnType<typeof test>;

/** 获取promise结果类型 */
type TestPromiseType = Awaited<TestReturnType>;

Pick 从一个类型中提取指定属性并返回新的类型

这个也还是挺有用,用视频中的例子,我们在封装的时候,对于参数的类型,常常会有选填的,但是这个选填并不是真的不填,而是我们预设了一个默认配置对象,实际在使用的时候是将默认配置对象与用户传入的配置对象进行合并,最后生成实际使用的配置。

interface Options {
  method?: "GET" | "POST";
  url: string;
  data?: any;
}

/** 默认配置 */
const defaultOptions = {
  method: "GET",
};

function test(options: Options) {
  return { ...defaultOptions, ...options };
}

但是我们查看函数返回值,会发现method的类型变成string,因为默认配置的method是string类型,合并的时候string的适用范围更广,于是使用了string作为method的类型。

如果默认配置是一个固定值的类型,那么其实会发生合并。

interface Options {
  method?: "GET" | "POST";
  url: string;
  data?: any;
}

/** 默认配置 */
const defaultOptions: { method: "PUT" } = {
  method: "PUT",
};

function test(options: Options) {
  return { ...defaultOptions, ...options };
}

我们可以使用Pick提取出method的类型声明,这样就不用再书写重复的代码。

interface Options {
  method?: "GET" | "POST";
  url: string;
  data?: any;
}

/** 默认配置 */
const defaultOptions: Pick<Options, "method"> = {
  method: "GET",
};

function test(options: Options) {
  return { ...defaultOptions, ...options };
}

这样method的类型定义就是正确的了,但是还是会有一个问题,由于它是一个可选项,所以它有可能是undefined类型,我们的defaultOptions也可以将method设置为undefined的。

/** 默认配置 */
const defaultOptions: Pick<Options, "method"> = {
  method: undefined,
};

显然这是不对的。

Required 将所有属性变为必填属性

书接上文,我们可以利用Required工具来完善。

interface Options {
  method?: "GET" | "POST";
  url: string;
  data?: any;
}

/** 默认配置 */
const defaultOptions: Required<Pick<Options, "method">> = {
  method: "GET",
};

function test(options: Options) {
  return { ...defaultOptions, ...options };
}

问题被完美解决。

Omit 排除指定属性并生成新的对象类型

Omit和Picks是相反的作用,我用Omit实现过一个axios的GET函数参数定义,因为get请求其实从语义上讲,你只能传递链接参数,data这个参数是不能使用的,但是axios做了容错处理,它允许你在get请求中使用data,但是并不推荐这么干。

在之前的项目中看有人在get请求中使用data属性,给我难受坏了。

interface Options {
  method?: "GET" | "POST";
  url: string;
  data?: any;
}

/** 默认配置 */
const defaultOptions: Required<Omit<Options, "url" | "data">> = {
  method: "GET",
};

function test(options: Options) {
  return { ...defaultOptions, ...options };
}

Record 创建一个key不同但值类型相同的对象类型

这个其实在之前的文章中提过这个工具类型,但是记不住,使用率一直不高,记不住的原因一方面是没找到合适的使用场景,另一方面是当时基础还不行。

这次视频提供的这个例子非常好,我就照搬一下。

当我们使用i18n去做国际化的时候,可以使用js或者ts文件导出一个对象作为某一个语言的翻译文本,假设我们支持3个国家,那么最终组合出来的对象格式如下:

const i18n = {
  zh: {},
  en: {},
  jp: {},
};

zh、en、jp的对象类型其实都是一样的,为此,我们可以通过Record快速实现这种类型定义。

type I18nKeys = "zh" | "en" | "jp";

interface I18nValue {
  name: string;
}

type I18n = Record<I18nKeys, I18nValue>;

const i18n: I18n = {
  zh: {
    name: "中文",
  },
  en: {
    name: "English",
  },
  jp: {
    name: "日本語",
  },
};

其实除了创建值相同的这种情况,还有一种偷懒用法,比如我们写demo的时候声明对象类型:

const a: Record<string, string> = {
  a: "a",
};

这种方式可以省的去单独写一个type或者interface了,直接一行搞定,而且Record的K,V泛型是支持联合类型声明的,灵活度还是很高的。

Partial 将所有属性设置为可选并返回新的类型

这个发音是真难读啊。

有这么一种场景,我们会有一个基础对象,然后给用户提供一个可选对象,属性可能都一样,我们不需要再单独写一份类型声明,可以通过Partial转换一下。

interface Todo {
  id: number;
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Omit<Todo, "id">>) {
  return { ...todo, ...fieldsToUpdate };
}

updateTodo({ id: 1, title: "test", description: "test" }, {});

此时第二个参数对象的内容可以为空,也可以是title或者description,或者两者都有。

Exclude 排除指定联合类型并生成新的类型

type Names = "Alice" | "Bob" | "Eve" | "Mallory";

/** 排除指定联合类型 */
type ANames = Exclude<Names, "Eve">;
type BNames = Exclude<Names, "Eve" | "Mallory">;

Extract 提取指定联合类型并生成新的类型

type Names = "Alice" | "Bob" | "Eve" | "Mallory";

/** 提取指定联合类型 */
type CNames = Extract<Names, "Eve">;
type DNames = Extract<Names, "Alice" | "Bob">;

NonNullable 排除联合类型中的null和undefined并返回新的类型

type TestType = string | number | boolean | null | undefined;

/** 排除null和undefined类型 */
type AType = NonNullable<TestType>;

注意只能用于联合类型。

Readonly 将所有属性设为只读并返回新的属性

interface A {
  name: string;
  age: number;
  type: number;
  data: {
    name: string;
  };
}

/** 只读 */
type ReadonlyA = Readonly<A>;

Readonly只能接一个泛型,之前自己好像沙雕了,一直想省点事,想着Readonly接一个变量。

这个工具可以用于一些保护性的处理,比如我们导出一个常量对象,为了防止有人去改动它,我们将其定义为只读类型。

interface A {
  name: string;
  age: number;
  type: number;
  data: {
    name: string;
  };
}

/** 只读 */
type ReadonlyA = Readonly<A>;

const a: ReadonlyA = {
  name: "a",
  age: 1,
  type: 1,
  data: {
    name: "a",
  },
};

a.name = "";

当我们去修改属性时就会报错。

但是这种方式其实会有一些不足,因为ts官方提供的这些快捷工具,往往只能处理第一层的属性,当我去修改a.data.name时,这个name就不是只读属性了。

为此我们需要自定义一个辅助类型。

DeepReadonly 递归所有属性设置为只读并返回新的类型

这个功能官方是没有提供的,ts官方提供的所有批量设置的工具类型,都只能设置第一层属性,如果你期望哪怕是属性值是一个对象,这个对象也应该设置为相同的效果,就需要特殊处理了。

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

首先,我们需要了解 keyof 关键字的含义。keyof T 表示获取类型 T 所有属性的名称组成的联合类型。例如,对于以下类型:

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

keyof Person 的类型为 "name" | "age"

接着,我们来看 DeepReadonly 类型的定义:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

这个类型定义了一个泛型类型 DeepReadonly<T>,它的作用是将一个对象及其嵌套的属性都定义为只读。具体来说,它使用了 TypeScript 中的映射类型(Mapped Types)和条件类型(Conditional Types)。

首先,它使用了映射类型 [P in keyof T],表示对类型 T 的每个属性进行映射。对于每个属性 P,它将生成一个只读的属性,属性名称和类型与原来的属性相同。

然后,它使用了条件类型 T[P] extends object ? DeepReadonly<T[P]> : T[P],表示对属性的值进行条件判断。如果属性的值是一个对象,则递归地将它的属性都定义为只读;否则,保持原来的值不变。

最终,DeepReadonly<T> 的类型表示将类型 T 及其嵌套的属性都定义为只读。

关于 Deep 深度递归类型处理处理

我们可以基于上面这种DeepReadonly方式,实现很多种的变种,比如深度必填之类的,具体代码就不写了,都是重复性的代码。

(typeof Array)[number] 获取数组中的类型

const arr = [1, 2, 3, "4", true];

type MyType = (typeof arr)[number];

此时我们得到:string | number | boolean联合类型。

但是我们暂时还没有办法获取数组指定下标的值类型,所以如果你还想操作什么,可以使用上面提到过的,对于联合类型操作的工具。

分类: TypeScript 标签: excluderequiredTypeScriptReadonlyPartialPickRecordtypeof类型定义工具ParametersReturnTypeAwaitedOmitExtractNonNullableDeepReadonlyDeep

评论

暂无评论数据

暂无评论数据

目录