`createAction` 返回的类型不是通过 `ofType` 维护的

The type returned by `createAction` isn't maintained through `ofType`

我正在制作一个应用程序,让用户 select 从可用零食列表中选择零食。零食是从外部加载的 API.

我正在使用 redux-observable 来“侦听”操作,然后发送适当的 API 请求。这是我现有的代码,对有问题的代码行进行了评论:

// actions.ts

import { createAction } from '@reduxjs/toolkit';

import * as Type from './types';

const pfx = '[Snacks]';

export const snackSelect = createAction<Type.Snack['id']>(`${pfx} Select`);

export const snackSelectSuccess = createAction(`${pfx} Select success`);

export const snacksAvailableLoad = createAction(`${pfx} Available load`);

export const snacksAvailableLoadSuccess = createAction<Array<Type.Snack>>(`${pfx} Available load success`);
// epics.ts

import {
  combineEpics,
  ofType,
} from 'redux-observable';
import {
  map,
  switchMap,
} from 'rxjs/operators';

import * as API from '../api';

import * as Actions from './actions';

export default combineEpics(
  action$ => action$.pipe(
    ofType(Actions.snacksAvailableLoad),
    switchMap(API.snacksGet),
    map(Actions.snacksAvailableLoadSuccess),
  ),

  action$ => action$.pipe(
    ofType(Actions.snackSelect),
    switchMap(({ payload }) => API.snackSelect(payload)), // Error: Property 'payload' does not exist on type 'Action<any>'
    map(Actions.snackSelectSuccess),
  )

);

payload(应该是目标零食 ID)在我的史诗 switchMap 中不可用。

我可以用 switchMap((action: ReturnType<typeof Actions.snackSelect>) => API.snackSelect(action.payload)) 强制它,但是 ReturnType<typeof> 看起来像代码味道 and/or 而不是很多样板文件。

是否有正确的方法来通过 ofType 保留输入?

如果我使用 filter.match 则推断出正确的类型:

export default combineEpics(
  action$ => action$.pipe(
    filter(Actions.snacksAvailableLoad.match),
    switchMap(API.snacksGet),
    map(Actions.snacksAvailableLoadSuccess),
  ),

  action$ => action$.pipe(
    filter(Actions.snackSelect.match),
    switchMap(({ payload }) => API.snackSelect(payload)),
    map(Actions.snackSelectSuccess),
  ),
);

我不太熟悉 redux-observable,但看起来 ofType 函数中的过滤是基于 Action['type'] 属性 的类型。看我对类型函数声明的评论:

export declare function ofType<
  T extends Action,                     // input action type
  R extends T = T,                      // output action type
  K extends R['type'] = R['type']       // type name to filter by
>(...key: K[]): (source: Observable<T>) => Observable<R>;

第一个问题是,根据您的 TypeScript 版本,您操作的 type 属性 只是 string 而不是像 "[Snacks] Select success" 这样的字面值。当您使用 ${pfx} Select success 这样的模板创建字符串时,您需要添加 as const 以避免扩大到 string。这在 v4.3.0+(目前处于测试阶段)中不是必需的。

添加 as const 将修复您的两个没有负载的操作。确实有有效载荷的两个仍然是 string。这是因为 createAction 函数有两个泛型类型参数,用于负载 P 和类型 T。您不能指定一个参数并推断出第二个参数。因此,当您为有效负载设置泛型时,您将失去推断字符串文字类型的能力,它将回退到默认值 string.

您始终可以手动指定两者:

export const snackSelect = createAction<Type.Snack["id"], `${typeof pfx} Select`>(
  `${pfx} Select` as const
);

或者创建某种辅助函数,因为看起来你在这里有一个一致的模式。

第二个问题是 ofType 仅从类型名称 K 推断操作类型 R 的能力。如果您的初始操作类型 T 是您特定操作类型的联合,那么这可能会通过提取可分配给 {type: K} 的联合成员自动发生。但是如果 T 是一般的 any 动作那么它将无法区分它。

所以你需要使用某种类型保护。 Redux-toolkit 操作带有内置类型保护功能 match.

declare interface BaseActionCreator<P, T extends string, M = never, E = never> {
    type: T;
    match(action: Action<unknown>): action is PayloadAction<P, T, M, E>;
}

...显然这就是您所需要的,这也解决了第一个问题。