TypeScript 重构是一个很大的话题,很难三言两语说得清楚。下面以一个简单的 React 开发的计数器应用来谈一谈怎样使用 TypeScript 循序渐进的重构 React 应用,怎样更好的使用 TypeScript 类型系统让重构更“丝滑”。
先看看重构前的计数器应用代码,这是一个学习 React useReducer Hook 的很好案例。
现在要用 TypeScript 来重构计数器。得益于 React Hooks 对 TypeScript 的强力支持,重构应该不难:基本上就是改改文件后缀,加加类型定义的事。?
使用类型断言
- 为
reducer
函数的state
和action
参数添加如下类型定义(请参看 App.tsx):
interface State {
count: number;
}
interface Action {
type: "increment" | "decrement" | "reset";
payload: number;
}
interface Props {
dispatch: (action: Action) => void;
initialCount: number;
}
实际上,reset
action 具有 type
和 payload
两个属性。而 increment
action 和 decrement
action 只有 type
属性,没有有 payload
属性。所以使用 dispatch
函数调度 increment
和 decrement
两个 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 分成了 ChangeAction
和 ResetAction
两组——只有 ResetAction
有 payload
属性。调用类型时还是使用 Action
。充分利用了联合类型中子类型可以有不同的成员的特性,既省去了导入泛型对 Action type
需要做条件检查,又不用对 payload
属性做类型断言。所以联合类型是更好的方案,它比类型断言方案安全,比泛型条件类型方案简洁。
重构一个原则是:改动越少越“丝滑”✌️
参考项目
访问 codesandbox 查看项目代码。
评论 (0)