网站搜索

javascript - 在 TypeScript 中封装嵌套的函数对象?


我正在使用 Redux,并已将存储数据拆分为“切片”。每个“切片”对应一个区域,例如 users 或 comments。我将每个切片的选择器组合成一个顶层 selectors 模块,并在整个应用程序中访问它。

一个切片的格式是:

export interface IUsersSelectors {
  getCurrentUser(state: IUsersState): IUser | undefined;
}

const selectors: IUsersSelectors = {
  getCurrentUser(state: IUsersState) {
    return state.currentUser;
  }
};

export default {
  getInitialState,
  reducer,
  selectors
};

然后全部导入,并将选择器组合起来:

export const selectors = Object.keys(slices).reduce((combinedSelectors: any, sliceKey: string) => {
  const sliceSelectors = slices[sliceKey].selectors;

  combinedSelectors[sliceKey] = Object.keys(sliceSelectors).reduce((selectorsMap: object, selectorKey: string) => {
    const localizedSelector = sliceSelectors[selectorKey];

    selectorsMap[selectorKey] = (globalState, ...args: any[]): any => {
      return localizedSelector(globalState[sliceKey], ...args);
    };

    return selectorsMap;
  }, {});

  return combinedSelectors;
}, {});

然后在整个应用中使用:

selectors.users.getCurrentUser(store.getState());

这意味着选择器 期望 在获取数据时只使用它们的片段状态,但它们实际上是用全局存储状态被 调用 的。我本质上只是将它们包装在另一个管理作用域的函数中。

我接近于为此定义泛型类型的是:

type IScopedSelector<T extends () => any> = (globalState: IStoreState, ...args: any[]) => ReturnType<T>;

type IScopedSelectors<T> = {
  [K in keyof T]: IScopedSelector<T[K]>;
};

type INestedScopedSelectors<R> = {
  [S in keyof R]: IScopedSelectors<R[S]>;
};

export const selectors: INestedScopedSelectors<ISelectors>...

其中 ISelectors 是一个形状如下的简单接口:

export interface ISelectors {
  users: IUsersSelectors;
}

但是,当尝试将 T[K] 传递给 IScopedSelector 时,我会遇到这个类型错误,因为它必须是一个函数:

[ts] Type 'T[K]' does not satisfy the constraint '() => any'.

如果我移除 extends() => any,那么我会得到关于 ReturnType 的错误:

[ts] Type 'T' does not satisfy the constraint '(...args: any[]) => any'.

理想情况下,我也会保持选择器参数的类型(而不是 ...args: any[]),只将第一个参数重写为全局存储状态。

有更好的方式来处理像这样的嵌套泛型吗?这甚至可能吗?

答案#1

你需要为 IScopedSelectors 和 INestedScopedSelectors 的泛型类型添加类似的类型约束。

没有这样的约束,你就告诉 TypeScript,在 IScopedSelectors<T> 中的 T 可以是任何类型。因此 IScopedSelectors<string> 应该是有效的。但 TypeScript 正确地指出,对于许多类型(例如 string),[K in keyof T]: IScopedSelector<T[K]> 将不起作用,因为无法保证 T[K] 遵循 IScopedSelector 施加的约束。

所以解决方案就是在两个接口上都添加一个约束,以便 TypeScript 拥有这个保证。为此,内置的 Record 类型可能会很有帮助。所以类似这样:

type IScopedSelectors<T extends Record<string, () => any>> = {
  [K in keyof T]: IScopedSelector<T[K]>; // T[K] is now guaranteed to adhere to () => any
};

type INestedScopedSelectors<R extends Record<string, Record<string, () => any>>> = {
  [S in keyof R]: IScopedSelectors<R[S]>; // Similarly, R[S] is now guaranteed to adhere to Record<string, () => any>, exactly what IScopedSelectors is expecting.
};

您可能想根据您的具体使用情况将 Record 类型替换为更具体的类型,但解决方案本质上是一样的。只需确保将您的约束向上完全传递到整个层级结构。

希望这有帮助!