如何优雅实现 redux 的 Action ts 类型

引子

在学习 react 的过程中经常听到过 "redux" 这个状态管理工具。但因为工作中我最常用的是 mobx 或者 Context,所以一直都没怎么了解 redux。

于是我就抱着试试的想法在新的需求中使用了 useReducer 做组件的状态管理(useReducer 实现了 redux 模式)。

不得不说,在 ts 项目中使用 redux,十分别扭,十分难受,如同踩💩。但是本文并不讨论 redux 的 Reducer 设计中存在的代码冗余。而是讨论如何制作一份精致的💩 ☝️🤓。

解类型问题的过程

常见无封装的 Action 类型写法

下面这种常见的"穷举" action 类型的方式显然需要不停地重复 type:xxx,action:xxx 显得代码比较呆,比较冗余。当然如果你不嫌弃,使用"CV大法"并不影响你的工作效率。

ts 复制代码
import { useReducer } from 'react';

type RunPayload = { distance: number };
type RunAction = {
  type: 'run';
  payload: RunPayload;
};
type FlyPayload = { height: number };
type FlyAction = {
  type: 'fly';
  payload: FlyPayload;
};
type Action = RunAction | FlyAction;

export const useFoo = () => {
  const [state, dispatch] = useReducer((state: any, action: Action) => {
    switch (action.type) {
      case 'run':
        // 处理 run,ts 可以根据 type 推出 payload 类型为 { distance: number }
       console.log( action.payload.distance);
        break;
      case 'fly':
        // 处理 fly
        console.log( action.payload.height);
        break;
      default:
        break;
    }
    return state;
  }, {});
};

使用工具类型对每个 Action 进行封装

这个方法我看到在很多 StackOverflow 社区的回答中都有提到,但我认为这并不是最完美的,而且组织形式也"惊为天人"。

ts 复制代码
import { useReducer } from 'react';

type RunPayload = { distance: number };

type FlyPayload = { height: number };
// 使用这个工具类型封装一个 Action
type BuildSingleAction<Type, Payload> = {
  type: Type,
  payload: Payload,
}

type Action = BuildSingleAction<'run', RunPayload>
  | BuildSingleAction<'fly', FlyPayload>

export const useFoo = () => {
  const [state, dispatch] = useReducer((state: any, action: Action) => {
    switch (action.type) {
      case 'run':
        // 处理 run,ts 可以根据 type 推出 payload 类型为 { distance: number }
       console.log( action.payload.distance);
        break;
      case 'fly':
        // 处理 fly
        console.log( action.payload.height);
        break;
      default:
        break;
    }
    return state;
  }, {});
};

当然一定会有一部分人说:"这代码太优雅了"。我并不这么认为,假如现在有 十几个 action,将会出现什么情况。我宣布 Action 类型大厦即将建成!!!

ts 复制代码
type Action = BuildSingleAction<'type1', Type1Payload>
  | BuildSingleAction<'type2', Type2Payload>
  | BuildSingleAction<'type3', Type3Payload>
  | BuildSingleAction<'type4', Type4Payload>
  | BuildSingleAction<'type5', Type5Payload>
  | BuildSingleAction<'type6', Type6Payload>
  | BuildSingleAction<'type7', Type7Payload>
  | BuildSingleAction<'type8', Type8Payload>
  | BuildSingleAction<'type9', Type9Payload>
  | BuildSingleAction<'type10', Type10Payload>
  | BuildSingleAction<'type11', Type11Payload>;

于是我就开始思考能不能有一种相对来说比较优雅的方式来表达 Action 这个联合类型呢?

我想到了 js 中的 Map。如果把类型表达成 key -> type, value -> payload,的方式。并有一个工具函数把这个类型 Map 转换成 Action,应该是个不错的选择。

解决方案

ts 复制代码
import { useReducer } from 'react';

type RunPayload = { distance: number };

type FlyPayload = { height: number };

/** 定义一个 key -> type, value -> payload  的 类型map*/
type Type2Payload = {
 run: RunPayload,
 fly: FlyPayload,
}

/** 定义一个工具类型将 Map 转为 Action */
type BuildActionFromMap<T extends Record<any, any>> = {
 [TypeKey in keyof T]: {
   type: TypeKey,
   payload: T[TypeKey],
 }
}[keyof T];

type Action = BuildActionFromMap<Type2Payload>;

export const useFoo = () => {
 const [state, dispatch] = useReducer((state: any, action: Action) => {
   switch (action.type) {
     case 'run':
       // 处理 run,ts 可以根据 type 推出 payload 类型为 { distance: number }
      console.log( action.payload.distance);
       break;
     case 'fly':
       // 处理 fly
       console.log( action.payload.height);
       break;
     default:
       break;
   }
   return state;
 }, {});
};

BuildActionFromMap 类型做了什么

ts 复制代码
/** 定义一个工具类型将 Map 转为 Action */
type BuildActionFromMap<T extends Record<any, any>> = {
  /** 
   * 1. 通过 in 关键字构建 key -> { type: key, payload:T[key] } 的嵌套类型 map
   *    这是因为 in 关键字只能在对象类型的 "[key]:..." 位置使用
   *    (in 关键字直接对联合类型使用获取到值不具备类型含义,其只是一个遍历标识)
   */
  [TypeKey in keyof T]: {
    type: TypeKey,
    payload: T[TypeKey],
  }
  /**
   * 2. 由于我们想要的是这个嵌套类型的 value 部分,
   *    我们可以通过 keyof 获取到其 key 再通过[key] 获取 value
   *    这时我们获取到的就是 Action 对应的联合类型了 
   */
}[keyof T];

总结

ts 类型真是技术活,人生苦短,我选 Any Script 👻

相关推荐
凤城老人1 小时前
从 PyQt6 到 Electron:给 Edge TTS 做一个“多角色配音机“的踩坑手记
javascript·typescript·electron
zhousenshan1 天前
TypeScript 入门到精通
typescript
花旗蜕变1 天前
前端转 NestJS 全栈实践:从表单页面到微信业务系统
前端·typescript·nestjs
艺杯羹2 天前
Anthropic突袭发布Claude Code Mods!用TypeScript编写智能体中间件:告别AI误删与失控
人工智能·typescript·系统架构
FYKJ_20104 天前
SSM校园失物招领系统41452-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·typescript·spark
tiankongdeyige5 天前
游戏战斗系统学习第2篇:有限状态机 (FSM)
学习·游戏·typescript
FYKJ_20105 天前
django电影推荐系统55066-计算机课程设计、毕业设计
vue.js·spring boot·python·mysql·typescript·spark·django
进击的明明5 天前
TypeScript速通笔记(下)
前端·面试·typescript
愛芳芳5 天前
element-plus+vite+vue3+pinpa+mock+typeScript搭建经典的电商管理后台
前端·javascript·typescript
miss5 天前
从零做一个可视化规则引擎:Vue3 递归条件树 + 双引擎结果对比
前端·vue.js·typescript