TypeScript 重构

Flying
2021-11-05 / 0 评论 / 110 阅读 / 正在检测是否收录...

TypeScript 重构是一个很大的话题,很难三言两语说得清楚。下面以一个简单的 React 开发的计数器应用来谈一谈怎样使用 TypeScript 循序渐进的重构 React 应用,怎样更好的使用 TypeScript 类型系统让重构更“丝滑”。

先看看重构前的计数器应用代码,这是一个学习 React useReducer Hook 的很好案例。

enhancement-ts.svg

现在要用 TypeScript 来重构计数器。得益于 React Hooks 对 TypeScript 的强力支持,重构应该不难:基本上就是改改文件后缀,加加类型定义的事。?

使用类型断言

  • reducer 函数的 stateaction 参数添加如下类型定义(请参看 App.tsx):
interface State {
  count: number;
}

interface Action {
  type: "increment" | "decrement" | "reset";
  payload: number;
}

interface Props {
  dispatch: (action: Action) => void;
  initialCount: number;
}

实际上,reset action 具有 typepayload 两个属性。而 increment action 和 decrement action 只有 type 属性,没有有 payload 属性。所以使用 dispatch 函数调度 incrementdecrement 两个 action 时会抛出参数类型错误:

dispatch({ type: 'increment' })}
// 类型“{ type: "increment"; }”的参数不能赋给类型“Action”的参数。
//  类型 "{ type: "increment"; }" 中缺少属性 "payload",但类型 "Action" 中需要该属性。

这个问题很好解决。payload 设置成 ?. 可选就行了。但新问题来了:init 函数抛出参数类型错误:

function reducer(state: State, action: Action) {
  switch (action.type) {
  // ...
    case 'reset':
      return init(action.payload);
      // 类型“number | undefined”的参数不能赋给类型“number”的参数。
      //   不能将类型“undefined”分配给类型“number”。
  // ...
}

这个问题也不难解决。将 action.payload 断言成 number 类型即可。

return init(action.payload as number);
也可以使用 ! 非空断言,不过可能需要额外处理一下 eslint 警告。

这种方案在定义好类型,然后根据错误提示补充类型注解,最关键的改动是加了一个断言类型。还是比较简单的,不过有些“头痛医头,脚痛医脚”的感觉。有更好的方案吗?答案是肯定的。

TypeScript 最佳实践通常是尽可能避免使用类型断言。最好是完全类型的代码,并且不需要使用断言干扰 TypeScript 对其类型的理解。但偶尔会出现类型断言很有用,甚至是必要的情况。

使用泛型条件类型

条件类型能够检查其作用域中的任何类型名称,包括条件类型本身的类型参数。这意味着你可以编写可重用的泛型类型,根据其他任何类型创建新的类型。条件类型的威力来自于与泛型一起使用。

泛型条件类型的详细用法,请参看上一篇文章

ConditionalTypes.tsx 中对 reducer 函数的参数类型定义做如下修改:

interface State {
  count: number;
}

interface IAction<T> {
  type: T;
  payload: number;
}

type ActionType = 'increment' | 'decrement' | 'reset';

interface Props {
  dispatch: (action: Action<Type>) => void;
  initialCount: number;
}

type Action<T> = T extends 'reset' ? IAction<T> : Omit<IAction<T>, 'payload'>;
Omit<Type, Keys> 构造一个类型,它从 Type 中挑选出所有属性,然后删除 Keys(字符串字面量或字符串字面量联合)中包含的属性。

这里我们使用了泛型参数,所以抽出了一个 Type 字面量联合类型。调用时只需将先前的 Action 类型改为泛型版本即可。

function reducer(state: State, action: Action<T>) 
// ...

注意:类型定义最后一句使用了泛型条件类型,鼠标悬停到 reducer 函数的 action 参数上看看 TypeScript 创建了什么新的 Action 类型?

(parameter) action: IAction<"reset"> | Omit<IAction<"increment">, "payload"> | Omit<IAction<"decrement">, "payload">

这类型正是我们想要的,泛型条件类型方案最关键的改动是调用类型时需要使用 Action 泛型版本。该方案确实是使用完全类型的代码,构造也比断言方案的清晰很多。但改动不比断言方案少,有比这的方案吗?答案是肯定的。

使用联合类型

联合类型可以将一个值的允许类型扩展为两个或更多可能的类型。联合类型实质是是有条件的类型并集,可以看作是泛型条件类型的自动实现。

UnionTypes.tsx 中,对类型做如下定义:

interface State {
  count: number;
}

type ChangeAction = {
  type: 'increment' | 'decrement';
};

type ResetAction = {
  type: 'reset';
  payload: number;
};

type Action = ChangeAction | ResetAction;

interface Props {
  dispatch: (action: Action) => void;
  initialCount: number;
}

这里,我们将 Action 分成了 ChangeActionResetAction 两组——只有 ResetActionpayload 属性。调用类型时还是使用 Action。充分利用了联合类型中子类型可以有不同的成员的特性,既省去了导入泛型对 Action type 需要做条件检查,又不用对 payload 属性做类型断言。所以联合类型是更好的方案,它比类型断言方案安全,比泛型条件类型方案简洁。

重构一个原则是:改动越少越“丝滑”✌️

参考项目

访问 codesandbox 查看项目代码

1

评论 (0)

取消