类型检查 createAction Redux 助手

Typecheck createAction Redux helper

Action类型定义如下:

type Action =
    { type: 'DO_X' }
    |
    { type: 'DO_Y', payload: string }
    |
    { type: 'DO_Z', payload: number }

这是一种联合类型,其中每个成员都是一个有效的操作。

现在我想创建一个接受 type 的函数 createAction 和 returns 一个接受 payload.

的新函数
const doZ = createAction('DO_Z')
console.log(doZ(42)) // { type: 'DO_Z', payload: 42 }

这是我当前的实现:

const createAction = (type: Action['type']) =>
  (payload?: any) =>
    ({ type, payload })

它会像我想要的那样进行类型检查 type。我怎样才能对 payload 进行类型检查?我希望 payload 匹配基于 type 的正确操作类型。例如,doZ 在使用 string 调用时应该会失败,因为它 payload 表示它只接受 number.

冗长但有效:

type XAction = { type: 'DO_X', payload: undefined }; 
type YAction = { type: 'DO_Y', payload: string }; 
type ZAction = { type: 'DO_Z', payload: number }; 

type Action = XAction | YAction | ZAction;

const createAction = <T extends Action>(type: T['type']) =>
    (payload: T['payload']) =>
        ({ type, payload });

// Do compile:

createAction<XAction>("DO_X")(undefined);
createAction<YAction>("DO_Y")("foo");
createAction<ZAction>("DO_Z")(5);

// Don't compile:

createAction<XAction>("DO_X")(5); // Expected `undefined`, got number
createAction<YAction>("DO_Y")(5); // Expected string, got number
createAction<ZAction>("DO_X")(5); // Expected `"DO_Z"`, got `"DO_X"`

更简单的方法(不强制createAction的类型参数):

type Action = { type: 'DO_X', payload: undefined } | { type: 'DO_Y', payload: string } | { type: 'DO_Z', payload: number };

createAction("DO_Y")("foo");

不幸地允许 createAction<YAction>("DO_Y")(5) 等编译,因为 T 总是被推断为 Action 因此 payload 参数是 string|number|undefined

这个问题的规范答案取决于您的确切用例。我将假设您需要 Action 来准确评估您编写的类型;也就是说,type: "DO_X" 的对象 而不是 具有任何类型的 payload 属性。这意味着 createAction("DO_X") 应该是零参数的函数,而 createAction("DO_Y") 应该是单个 string 参数的函数。我还将假设您希望自动推断 createAction() 上的任何类型参数,这样您就不需要为 [=23= 的任何值指定 createAction<Blah>("DO_Z") ].如果解除这些限制中的任何一个,您可以将解决方案简化为@Arnavion 给出的解决方案。


TypeScript 不喜欢从 属性 values 映射类型,但很高兴从 属性 keys。因此,让我们以一种为我们提供编译器可以用来帮助我们的类型的方式构建 Action 类型。首先,我们像这样描述每个动作类型的有效负载:

type ActionPayloads = {
  DO_Y: string;
  DO_Z: number;
}

让我们也介绍任何 Action 没有有效负载的类型:

type PayloadlessActionTypes = "DO_X" | "DO_W";

(我添加了一个 'DO_W' 类型只是为了展示它是如何工作的,但你可以删除它)。

现在我们终于可以表达 Action:

type ActionMap = {[K in keyof ActionPayloads]: { type: K; payload: ActionPayloads[K] }} & {[K in PayloadlessActionTypes]: { type: K }};
type Action = ActionMap[keyof ActionMap];

ActionMap 类型是一个对象,其键是每个 Actiontype,其值是 Action 联合的相应元素。它是 Actionpayload 的交集,以及 Actionpayload 的交集。而Action就是ActionMap的值类型。验证 Action 是否符合您的预期。

我们可以使用ActionMap来帮助我们输入createAction()函数。这是:

function createAction<T extends PayloadlessActionTypes>(type: T): () => ActionMap[T];
function createAction<T extends keyof ActionPayloads>(type: T): (payload: ActionPayloads[T]) => ActionMap[T];
function createAction(type: string) {
  return (payload?: any) => (typeof payload === 'undefined' ? { type } : { type, payload });
}

这是一个重载函数,其类型参数 T 对应于您正在创建的 Actiontype。上面的两个声明描述了这两种情况:如果 T 是没有 payloadActiontype,则 return 类型是零参数函数return输入 Action 的正确类型。否则,它是一个单参数函数,它采用 payload 的正确类型,return 是 Action 的正确类型。实现(第三个签名和正文)与您的类似,只是如果没有传入 payload,它不会将 payload 添加到结果中。


大功告成!我们可以看到它按预期工作:

var x = createAction("DO_X")(); // x: { type: "DO_X"; }
var y = createAction("DO_Y")("foo"); // y: { type: "DO_Y"; payload: string; }
var z = createAction("DO_Z")(5); // z: { type: "DO_Z"; payload: number; }

createAction("DO_X")('foo'); // too many arguments
createAction("DO_X")(undefined); // still too many arguments
createAction("DO_Y")(5); // 5 is not a string
createAction("DO_Z")(); // too few arguments
createAction("DO_Z")(5, 5); // too many arguments

您可以看到这一切的实际效果 on the TypeScript Playground。希望对你有效。祝你好运!