浅谈高效写 type 的几种姿势

616 阅读8分钟

TypeScript本身更像是一个工具,并没有很大的技术含量在里面值得讨论,值得讨论的是,如何高效地善用这个工具罢了。

究其根本,TypeScript(TS)的就是一个关于 Type 的工具,在实践中,我们为数据写好与之对应的数据结构就可以了,之后一系列的事,TypeScript 都会帮我们做好,因此,相对于编程来说,TS真是再简单不过了。

简单在于在框架加持下,我们的脚手架都已经配置好了 TS 相关配置,我们只要写写 interface 和 type就好了,再加上一些联合类型以及交叉类型,对了,泛型也不能少,这就足够可以满足大多数应用场景了,因此,TS 是不难的,即便没有强类型语言基础的人也不会很难,因为一切需要我们手动编写的 Type 皆可测。

只有人心难测,因为即便难测的 Type 也有 never 可以兜底处理。

人人都会写 type,因此我想谈的不是怎么写 type,是如何在实践中高效地写 type。

1.善用内置类型和类型推断

  • 善用 typeof,keyof 这两个类型操作符, 如果对于已经有数据结构的变量,可就别老老实实一笔一划写 type,一个 typeof 直接搞定不香吗?keyof 同理,对于已经定义好 key 的变量,直接获取就完善了,没必要搞个联合类型或者枚举类型了,这两个类型操作符可以说是我在日常用的频率最高的了。

  • 一定不要小看内置类型,内置类型又有那么多,哪些重要呢?我先随便举两个例子吧 例子1:在后台管理系中,如果我们在用户列表中定义用户的类型,我们可能会像下面这样定义

export interface UserListItem  {
  id:number
  userId: number
  email: string
  mobile: string
  description: string
  status: number
  sex: number
}

定义好以后,我们固然可以读取到每一行的user 属性,但是当我们更改的时候,我们可能只需要向后台传部分数据,如修改状态,就只需要传 id、status 这两个字段,难道我们要单独定义一个type UpdateUserStatus = {id:number,status:number}这样的类型吗?事实上是不必要的,TS 为了减少定义重复子结构类型所带来的心智负担,已经为我们准备了诸多内置类型,这里我们只需要用到 Patrial 这个类型转换一下就好了。

Patrial:构造一个所有属性都Type设置为optional的类型。该实用程序将返回一个表示给定类型的所有子集的类型。

如上转换一下就好了,这些属性就都变成了可选属性,TS 又只检查接口对应需要的属性,而不会检查多余的属性,因此一下子就达到了类型转换的功能,与之功能相反的类型是 Require

Required<Type>
构造一个类型,该类型由Typeset的所有属性设置为required。与之相反Partial。
例子
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
const obj2: Required<Props> = { a: 5 };
Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>’.

顾名思义,这个类型能把接口上所有 optionnal 的属性转换成必须,诸如此类的类型还有很多

  • Record<Keys,Type> 构造一个对象类型,其属性键为Keys,属性值为Type。该实用程序可用于将一个类型的属性映射到另一个类型。
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
cats.boris;
// ^ = const cats: Record
  • Pick<Type, Keys> 通过Keys从中选择属性集来构造类型Type。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
todo;
// ^ = const todo: TodoPreview
  • Omit<Type, Keys> 通过从中选择所有属性Type,然后移除来构造一个类型Keys。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
todo;
// ^ = const todo: TodoPreview

除了以上这些,还有 Extract,Exclude 等等这些高频的工具类型,以帮助我们更加高效转换类型,更多详细关于 Utility Types => Utility Types 手册

2.学会使用 infer

如果说 TS 有难点,那么到目前为止,我认为 infer 的用法差不多可以说是最难的,如果可以很熟练的使用 infer,我认为 TS 水平一定不会差到哪里,相应的,如果我是面试官,我要用一道题来考察一个人的 TS 水平怎么样,那么毫无疑问是考察infer了,下面就先介绍一下 infer。

  • 条件类型 因为infer 只能在条件类型中使用,首先先介绍一下条件类型(类似三目运算符)。 直接上例子
interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

type Example1 = Dog extends Animal ? number : string;
//   ^ = type Example1 = number

type Example2 = RegExp extends Animal ? number : string;
//   ^ = type Example2 = string

条件类型采用某种形式,看起来类似于condition ? trueExpression : falseExpressionJavaScript中的条件表达式

SomeType extends OtherType ? TrueType : FalseType;

仔细看上面几个例子应该就明白条件类型是怎么回事了吧,如果还没看懂,那就再仔细一点看,但凡是懂三目运算符的都看得明白,只不过 condition 这个判断表达式的形式永远是 A 类型 extends B 类型,因为TS 好像目前只有这个操作符有 true or false 判断,它和我们程序中的 a===b、b==c、'abc'==='abcd'这些表达式都是一样的,只不过把具体变量的值换成了操作 type,判断 typeA 是否是 TypeB 的子类,形如 TypeA extends TypeB,这就是条件类型,举个例子

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

interface Email {
  message: string;
}

interface Dog {
  bark(): void;
}

type EmailMessageContents = MessageOf<Email>;
//   ^ = type EmailMessageContents = string

type DogMessageContents = MessageOf<Dog>;
//   ^ = type DogMessageContents = never
  • infer 说完条件类型,再看来看看我们的 infer。

通常,条件类型的检查会为我们提供一些新信息。就像使用类型保护缩小范围可以为我们提供更特定的类型一样,条件类型的真实分支将通过我们检查的类型进一步约束泛型

让我们handlebook这个例子

type MessageOf<T> = T["message"];
Type '"message"' cannot be used to index type 'T'.

在此示例中,TypeScript错误,因为T不知道其名为的属性message。我们可以约束T,而TypeScript将不再报错:

type MessageOf<T extends { message: unknown }> = T["message"];

interface Email {
  message: string;
}

interface Dog {
  bark(): void;
}

type EmailMessageContents = MessageOf<Email>;
//   ^ = type EmailMessageContents = string

再进一步(先啰嗦的直接跳到 infer 那一步,哈哈哈,我直接从handlebook 搬过来的,这一段)

但是,如果我们想MessageOf采用任何类型,并且默认为类似never某个message属性不可用的情况该怎么办?我们可以通过将约束移出并引入条件类型来做到这一点:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

interface Email {
  message: string;
}

interface Dog {
  bark(): void;
}

type EmailMessageContents = MessageOf<Email>;
//   ^ = type EmailMessageContents = string

type DogMessageContents = MessageOf<Dog>;
//   ^ = type DogMessageContents = never

我们只是发现自己使用条件类型来应用约束,然后提取出类型。这最终成为一种常见的操作,使得条件类型使操作变得更容易。 条件类型为我们提供了一种使用infer关键字从真实分支中进行比较的类型推断的方法。例如

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

我们的内置类型 ReturnType便是用 infer 实现,下面用 infer 实现一遍

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

type Num = GetReturnType<() => number>;
//   ^ = type Num = number

type Str = GetReturnType<(x: string) => string>;
//   ^ = type Str = string

type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
//   ^ = type Bools = boolean[]

一句话,infer 就是将条件分支里的类型推导了出来,并且可以多次使用条件类型以达到复杂的操作,不知道你们懂了没有,下面多用几个例子说明一下

// 自己定义的 parameters,前置知识:MyParameters
type MyParameters<T extends (...args: any[]) => any> = T extends (
  ...args: infer U
) => any
  ? U
  : any;

const fun3 = (hello: 3, acttion: 'kkk') => {};
type Fun3 = (name: number, acttion: string) => any;
type P7 = Parameters<Fun3>;

let p7: P7 = [12, '23'];
//  P7 = [name:number,acttion:string]

可以嵌套条件类型以形成一系列按顺序求值的模式匹配:

type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;
type T0 = Unpacked<string>;
// ^ = type T0 = string
type T1 = Unpacked<string[]>;
// ^ = type T1 = string
type T2 = Unpacked<() => string>;
// ^ = type T2 = string
type T3 = Unpacked<Promise<string>>;
// ^ = type T3 = string
type T4 = Unpacked<Promise<string>[]>;
// ^ = type T4 = Promise
type T5 = Unpacked<Unpacked<Promise<string>[]>>;
// ^ = type T5 = string尝试

lodash的 get,第一眼看上去,的确有些吃力

type PropType<T, Path extends string> = string extends Path
  ? unknown
  : Path extends keyof T
  ? T[Path]
  : Path extends `${infer K}.${infer R}`
  ? K extends keyof T
    ? PropType<T[K], R>
    : unknown
  : unknown;

declare function get<T, P extends string>(obj: T, path: P): PropType<T, P>;
const obj = { a: { b: { c: 42, d: 'hello' } } };
const value = get(obj, 'a.b.d');
// value:string
const value1 = get(obj, 'a.b.c');
// value1:number

关于 infer 更多资料 => www.typescriptlang.org/docs/handbo…

3.泛型、never,unknown,字符串模版类型

  • 泛型 为了更高效的编写 type,泛型一定是必不可少的,可以让我们少定义很多重复结构的类型,这个应该就很简单了,就不详细说了。

  • never

简单来说就是永远不会发生的类型,对我而言,可以做兜底的错误处理,特别是分支处理中。

interface Foo {
  type: 'foo';
}

interface Bar {
  type: 'bar';
}

type All = Foo | Bar;

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break;
    case 'bar':
      // val 在这里是 Bar
      break;
    default:
      // val 在这里是 never
      const exhaustiveCheck = val;
      console.log(exhaustiveCheck);
      break;
  }
}
  • unknown

顶层类型,我可以通过收缩类型来确定类型,以继续操作,如果不想使用 any 以致丢失类型的话。

  • 字符串模版类型

使得拼错字符串的类型有救了,如 vuex 的子module下的 mutations 和 actions,也因为如此,本来 vuex5才完全支持 TS 的似乎要提到 vuex4.1版本了,可以关注一下。

后面这些说的很简单,但真要用得恰到好处,需要自己去实践中大量摸索。 type 只是工具,但是用得不好,反而带来了心智负担,希望我们都能高效编写 type,善用工具,毕竟,Lift is short.