【问题标题】:Correct TypeScript Type for thunk dispatch?thunk 调度的正确 TypeScript 类型?
【发布时间】:2019-03-29 09:08:24
【问题描述】:

我有一个异步的 redux 操作,所以我正在使用 thunk 中间件。

我有一个组件的mapStateToPropsmapDispatchToPropsconnect 函数如下:

const mapStateToProps = (store: IApplicationState) => {
  return {
    loading: store.products.loading,
    products: store.products.products
  };
};
const mapDispatchToProps = (dispatch: any) => {
  return {
    getAllProducts: () => dispatch(getAllProducts())
  };
};
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ProductsPage);

这一切都有效,但我想知道是否可以替换 mapDispatchToProps 中调度参数上的 any 类型?

我尝试了ThunkDispatch<IApplicationState, void, Action>,但在连接函数上出现以下 TypeScript 错误:

Argument of type 'typeof ProductsPage' is not assignable to parameter of type 'ComponentType<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>>'.
  Type 'typeof ProductsPage' is not assignable to type 'ComponentClass<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>, any>'.
    Types of property 'getDerivedStateFromProps' are incompatible.
      Type '(props: IProps, state: IState) => { products: IProduct[]; search: string; }' is not assignable to type 'GetDerivedStateFromProps<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>, any>'.
        Types of parameters 'props' and 'nextProps' are incompatible.
          Type 'Readonly<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>>' is not assignable to type 'IProps'.
            Types of property 'getAllProducts' are incompatible.
              Type '() => Promise<void>' is not assignable to type '() => (dispatch: Dispatch<AnyAction>) => Promise<void>'.
                Type 'Promise<void>' is not assignable to type '(dispatch: Dispatch<AnyAction>) => Promise<void>'.
                  Type 'Promise<void>' provides no match for the signature '(dispatch: Dispatch<AnyAction>): Promise<void>'.

是否可以替换mapDispatchToProps中dispatch参数上的any类型?

【问题讨论】:

  • getAllProducts thunk 的类型签名是什么?我有一个使用 typesafe-actions here 输入 redux 和 redux-thunk 的示例,但要看到全局可能会很棘手。查看其余相关代码可能会有所帮助。

标签: reactjs typescript redux redux-thunk


【解决方案1】:

这个设置非常适合我:

// store.ts
//...
export type TAppState = ReturnType<typeof rootReducer>;
export type TDispatch = ThunkDispatch<TAppState, void, AnyAction>;
export type TStore = Store<TAppState, AnyAction> & { dispatch: TDispatch };
export type TGetState = () => TAppState;
//...
const store: TStore = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(...middleware), ...enhancers)
);
export default store;

我的设置中的rootReducer 看起来像这样const rootReducer = createRootReducer(history);

// createRootReducer.ts
import { combineReducers, Reducer, AnyAction } from 'redux';
import { History } from 'history';
import {
  connectRouter,
  RouterState,
  LocationChangeAction,
} from 'connected-react-router';
// ... here imports of reducers
type TAction = AnyAction & LocationChangeAction<any>;

export type TRootReducer = Reducer<
  {
    // ... here types of the reducer data slices
    router: RouterState;
  },
  TAction
>;
const createRootReducer = (history: History): TRootReducer =>
  combineReducers({
    // ... here actual inserting imported reducers
    router: connectRouter(history),
  });

export default createRootReducer;

然后在连通分量中

import { connect } from 'react-redux';
import Add, { IProps } from './Add'; // Component
import { TDispatch, TAppState } from '../../store';

type TStateProps = Pick<
  IProps,
  'title' | 'data' | 'loading'
>;
const mapStateToProps = (
  state: TAppState,
): TStateProps => {
    // ... here you have typed state :)
    // and return the TStateProps object as required
    return {
      loading: state.someReducer.loading,
      //...
    }
}

type TDispatchProps = Pick<IProps,  'onSave'>;
const mapDispatchToProps = (
  dispatch: TDispatch,
): TDispatchProps => {
   // here you  have typed dispatch now
   // return the TDispatchProps object as required
   return {
    onSave: (): void => {
      dispatch(saveEntry()).then(() => {
        backButton();
      });
    },
   }
}

至于我做的thunk动作如下

// this is a return type of the thunk
type TPromise = Promise<ISaveTaskResponse | Error>;

export const saveEntry = (): ThunkAction<
  TPromise, // thunk return type
  TAppState, // state type
  any, // extra argument, (not used)
  ISaveEntryAction // action type
> => (dispatch: TDispatch, getState: TGetState): TPromise => {
  // use getState
  // dispatch start saveEntry action
  // make async call
  return Axios.post('/some/endpoint', {}, {})
    .then((results: { data: ISaveTaskResponse; }): Promise<ISaveTaskResponse> => {
      // get results
      // you can dispatch finish saveEntry action
      return Promise.resolve(data);
    })
    .catch((error: Error): Promise<Error> => {
      // you can dispatch an error saveEntry action
      return Promise.reject(error);
    });
};

【讨论】:

    【解决方案2】:

    Redux 可以分派普通对象的操作。假设我们有这样的行动{type: 'ACTION2'}。我们可以像这样创建动作创建器并将其包装在调度中

    // This is our action
    interface Action2 extends Action { type: "ACTION2"; }
    
    // And this is action crator
    const action2 = (): Action2 => ({ type: "ACTION2" });
    

    使用 thunk 中间件 Redux 可以调度函数。我们可以像这样创建异步操作

    // This action will be dispatched from async action creator
    interface Action1 extends Action { type: "ACTION1"; payload: string; }
    
    // And async action creator
    const thunkAction = (arg: string): ThunkAction<Promise<void>, {}, AppState, KnownActions> => 
        async dispatch => {
            const res = await asyncFunction(arg);
            dispatch({ type: "ACTION1", payload: res });
        },  
    

    这里使用ThunkAction&lt;R, S, E, A extends Action&gt; 类型。它接受以下类型参数:

    • R 保留内部函数的返回类型。在上面的例子中,内部函数是异步函数,所以它返回Promise&lt;void&gt;
    • S 保持应用状态
    • E 是未使用的扩展属性类型
    • A 是操作的类型。上面示例中的KnownActions 是所有可能操作类型的联合 (type KnownActions = Action1 | Action2;)

    现在让我们看看如何键入组件。

    interface Component1DispatchProps {
      action2: () => Action2;
      thunkAction: (arg: string) => Promise<void>;
    }
    
    interface Component1StateProps {
      stateprop: string;
    }
    
    const mapDispatchToProps = (
      dispatch: ThunkDispatch<AppState, {}, KnownActions>
    ) => {
      return {
        action2: () => dispatch(action2()),
        thunkAction: (arg: string) => 
          dispatch(thunkAction(arg))
      };
    };
    

    thunkAction 方法返回dispatch(thunkAction(arg)),其中thunkAction(arg) 返回ThunkAction&lt;Promise&lt;void&gt;, {}, AppState, KnownActions&gt;。所以dispatch 将使用ThunkAction 类型的参数调用,并将解析为

    <TReturnType>(
      thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, TBasicAction>,
    ): TReturnType;
    

    因此,dispatch(thunkAction(arg)) 在我们的示例中将返回 TReturnTypePromise&lt;void&gt;。在Component1DispatchProps 中为thunkAction 设置了这样的签名。

    完整的例子是here

    【讨论】:

      【解决方案3】:

      根据我的经验,问题不在于调度参数的类型,而实际上是组件上的 props 类型

      注意:我的解决方案没有使用 mapDispatchToProps 的函数表示法,因为在必要之前它是 not recommended(我还不需要它)。但我想这个解决方案可以适应它(拥有正确的沙箱)。

      要正确定义道具(无需创建复制粘贴界面),有必要mapDispatchToProps 分成两部分,一部分用于reducer 操作,另一部分用于thunk 操作

      为了展示这个解决方案,我从另一个答案中分出了一个例子(感谢@Fyodor)。

      完整代码在这里:https://codesandbox.io/s/thunkdispatch-forked-uss1b?file=/src/Component.tsx

      组件 props 是以下各项的组合:

      interface ComponentOwnProps {
        ownprop: string;
      }
      
      const mapStateToProps = (state: AppState) => ({
        stateprop: state.stateprop
      });
      
      const mapDispatchToProps = {
        action2
      };
      
      const mapDispatchThunkToProps = {
        thunkAction
      };
      

      props的完整类型定义为:

      type ComponentProps = ComponentOwnProps &
        ReturnType<typeof mapStateToProps> &
        typeof mapDispatchToProps &
        ThunkProps<typeof mapDispatchThunkToProps>;
      

      这里唯一的技巧是ThunkProps,它将字典中的 Thunk 操作类型从“双功能”(args) =&gt; (dispatch, getState) =&gt; void 转换为连接的(更简单的)(args) =&gt; void 功能。

      // types from redux examples (usually in `store.ts`)
      export type RootState = ReturnType<typeof store.getState>;
      export type AppThunk<ReturnType = void> = ThunkAction<
        ReturnType,
        RootState,
        any, // or some ThunkExtraArgument interface
        Action<string>
      >;
      
      // the tricky part: convert each `(args) => AppThunk => void` to `(args) => void`
      export type ThunkProps<T extends { [K in keyof T]: (...a: any[]) => AppThunk<void> }> = {
        [K in keyof T]: (...args: Parameters<T[K]>) => void;
      };
      

      ...终于可以连接组件了

      // don't forget to combine the two mapDispatch-es
      export default connect(
        mapStateToProps,
        {...mapDispatchToProps, ...mapDispatchThunkToProps}
      )(Component);
      

      完整代码(从@Fyodor 的答案分叉和更新):https://codesandbox.io/s/thunkdispatch-forked-uss1b?file=/src/Component.tsx ...为方便起见,所有重要的代码和类型都在Component.tsx 中。

      注意:尽管此解决方案有一些限制,但对我来说,它的目的是不编写不必要的接口并具有类型良好的道具,包括函数参数。

      【讨论】:

        【解决方案4】:

        您还可以在全局类型定义中为 redux Dispatch 函数提供另一个覆盖:

        (将其放在 global.d.ts 或其他一些全局类型定义中)

        import { ThunkAction } from 'redux-thunk';
        import { Action } from 'redux';
        
        declare module 'redux' {
            export interface Dispatch<A extends Action = AnyAction> {
                <T extends ThunkAction<any, any, any, any>>(action: T): T extends ThunkAction<infer K, any, any, any> ? K : never;
            }
        }
        

        然后在你的 mapDispatchToProps 中使用 Dispatch 类型而不是 ThunkDispatch。

        【讨论】:

          猜你喜欢
          • 2020-07-11
          • 2018-08-31
          • 2020-03-17
          • 2020-08-14
          • 2021-08-04
          • 1970-01-01
          • 2021-02-27
          • 2021-01-01
          • 1970-01-01
          相关资源
          最近更新 更多