如何流式键入对象映射,使值必须包含键

问题描述 投票:0回答:1

我正在尝试为redux正确键入(使用Flow)createReducer辅助函数。我用the code from redux-immutablejs作为起点。

我正在尝试跟随advice from the flow docs about typing redux reducers,但用简写地图样式将其包装在辅助函数中。基本思路是:

为每个操作创建单独的类型:

type FooAction = { type: "FOO", payload: boolean };
type BarAction = { type: "BAR", payload: string };

type Action = FooAction | BarAction;

将js对象图传递给createReducer,为每个动作提供缩小功能。

createReducer<Action>({}, {
  FOO: (state, action) => state,
  BAR: (state, action) => state,
})

我想从类型系统中看到什么:

  1. 在reducer函数中,action的输入正确(FOO看到FooAction),这样误用payload会产生错误。
  2. 如果地图包含额外(无效)键,则会出错
  3. 如果映射缺少键(对于定义的类型)则出错

我认为我无法弄清楚的部分是,我可以定义一个地图类型,以便键值必须包含在值类型中吗?

type Action<T = string> = { type: T, payload: any };
type Reducer<S, A> = (state: S, action: A) => S;
type HandlerMap<S, T> = {
  [T]: Reducer<S, Action<T>>,
};

我不认为上述内容非常有效,因为通用T将包含所有类型字符串,而不是指单个字符串。

try flow上查看此示例。我觉得它非常接近,但我无法摆脱所有类型错误。任何流动向导可以告诉我它是如何完成的?

javascript reactjs redux flowtype
1个回答
2
投票

虽然完全符合规定的问题(我相信)在当前版本的Flow中没有解决方案,但如果您愿意提前向Flow提供更多信息,我可以提供一种方法。

我认为我无法弄清楚的部分是,我可以定义一个地图类型,以便键值必须包含在值类型中吗?

对于像这样的任务,$ObjMap通常会成为您想要转向的工具。但$ObjMap只转换现有的地图类型,它只会转换它们的值(键 - 非自然地),所以这里还不够。诀窍是给Flow一个明确的动作名称到动作类型的映射:

type Actions /* helper type, never instantiated */ = {
  FOO: FooAction,
  BAR: BarAction,
};
type Action = $Values<Actions>; // = FooAction | BarAction;

我认为这不是太令人反感,因为你已经在你的例子中定义了type Action。鉴于您可以根据Action定义Actions,因此没有太多额外的冗余(以下关于将关键字与type属性相关联的讨论模块化 - 读取)。

给定这样的动作映射类型,我们可以按如下方式键入createReducer

type HandlerMap<O, S> = $Exact<$ObjMap<O, <A>(A) => (S, A) => S>>;
type Reducer<O, S> = (S, $Values<O>) => S;

declare var createReducer: <O, S>(
  initialState: S,
  handlers: HandlerMap<O, S>
) => Reducer<O, S>;

Here is a usage example:

type FooAction = { type: "FOO", payload: number };
type BarAction = { type: "BAR", payload: string };

type Actions /* helper type, never instantiated */ = {
  FOO: FooAction,
  BAR: BarAction,
};

const reducer = createReducer<Actions, $ReadOnlyArray<string>>(
  ([]: $ReadOnlyArray<string>),
  {
    FOO: (state, action) => [...state, action.payload.toFixed(0)],
    BAR: (state, action) => [...state, action.payload],
    // Flow will flag a type error if you uncomment the following line,
    // complete with the helpful message "property `BAZ` is missing in
    // object type but exists in object literal".
    //BAZ: (state, action) => state,
  }
);

let result: $ReadOnlyArray<string> = [];
result = reducer(result, { type: "FOO", payload: 6 });
result = reducer(result, { type: "BAR", payload: "six" });
// Each of the following lines flags a type error if uncommented.
//result = reducer(result, { type: "FOO", payload: "six" });
//result = reducer(result, { type: "BAR", payload: 6 });
//result = reducer(result, { type: "BAZ", payload: true });

请注意,Actions类型的键不必以任何方式与动作本身的type字段相关联。例如,如果我们改为使用动作类型定义

type FooAction = { type: "ONE", payload: number };
type BarAction = { type: "TWO", payload: string };

type Actions /* helper type, never instantiated */ = {
  FOO: FooAction,
  BAR: BarAction,
};

那么代码仍然可以成功地进行类型检查,但可能会在运行时失败(或者从不同的角度来看:实际上不可能用上述类型实现createReducer)。但不要担心,因为我们也可以要求Flow检查这个条件:

// Check that each the `type` property of each value in the action-map
// corresponds to that action's key.
(function<K: $Keys<Actions>>(
  x: $PropertyType<$ElementType<Actions, K>, "type">
): K {
  return x;
});

如果您不小心定义了一个没有type属性或type属性与Actions类型中的键不对应的动作,则上述操作将无法进行类型检查。该函数的存在证明了地图的构造是正确的。*

理想情况下,我们可以要求这个条件适用于与O一起使用的任何动作地图createReducer - 我们应该能够写

type HandlerMapCorrectnessProof<O> = <K: $Keys<O>>(
  $PropertyType<$ElementType<Actions, K>, "type">
) => K;

declare var createReducer: <O, S>(
  initialState: S,
  handlers: HandlerMap<O, S>,
  proof: HandlerMapCorrectnessProof<O>
) => Reducer<O, S>;

并且只是要求客户将(x) => x作为第三个参数传递给createReducer。但是,如果我们试图这样做,我们会遇到Flow issue #7548,所以现在你必须依靠外在验证你的行动图是一致的(通过人工检查或如上所述的个别静态检查)。

所有在一起,然后,here is a complete example that includes the static check

警告:在这个答案中构建的机制在很大程度上依赖于类型级操作,这种操作比大多数日常Flow代码更复杂。这有两个重要的含义:(a)它更有可能在Flow中遇到奇怪的错误(如上面的#7548),以及(b)对于不精通Flow或不习惯的合作者来说可能很难理解以这种方式思考类型。就个人而言,当我使用very simple subset of it, basically just disjoint unions of read-only exact objects时,我发现我从Flow中获得了最大的收益。我提出这篇文章是为了直接被要求回答这个问题,但我会毫不犹豫地将其投入生产。也就是说,这是一个合理的人可能在意见上有合理差异的话题。

*这个陈述不够精确到不正确 - 我真正的意思是身份函数具有这种类型的事实是所声称的证明,或者换句话说相关的identity type有人居住。 Flow没有讨论正确身份类型的词汇,但它的类型界限的表达足够强,我怀疑当前版本的Flow接受的这种类型的任何函数实际上必须是身份函数。

© www.soinside.com 2019 - 2024. All rights reserved.