首页 > 解决方案 > 如何使用 TypeScript 键入 Object 函数值的映射?

问题描述

我正在尝试通过组合减速器在 monorepo 应用程序中设置状态。目前,所有状态都按域划分,例如

type State = { fruit: FruitState, snacks: SnackState })

每个状态域都包含一些选择器。这些选择器以封装的方式定义,例如

const selectApples = (state: FruitState) => state.apples;

然后我们有一个web模块,它导入所有状态域选择器,按键对它们进行分组,然后将它们包装在一个高阶函数中,以将它们限定为域命名空间,例如

function scopeSelector<T extends keyof State>(
  scopeNamespace: T,
  selectors: { [selector: string]: Function }
) {
  return Object.keys(selectors).reduce((scoped, key) => ({
    ...scoped,
    [key]: (state: State) => selectors[key](state[scopeNamespace])
  }), {});
}

export const selectors = {
  fruits: scopeSelector('fruits', fruits.selectors),
  snacks: scopeSelector('snacks', snacks.selectors)
};

此代码在运行时工作 - 但会产生 TypeScript 错误,例如

// Error: Property 'selectApples' does not exist on type '{}'.
const apples = selectors.fruits.selectApples(state);

我尝试使用Ramda 的地图和npm-ramda的高级类型。这几乎奏效了,除了任何选择器的返回结果是其“范围”内所有选择器的联合。

在 StackBlitz 上建立了一个项目来演示这个问题。

标签: typescript

解决方案


TypeScript 无法推断{ [K in keyof typeof selector]: (state: RootState) => ReturnType<typeof selector[K]> }. reduce不幸的是,在使用构建对象时推断任何类型(甚至定义它们而不需要强制转换)几乎是不可能的。

也就是说,您可以通过一些强制转换来获得所需的行为,并手动声明返回类型。

function scopeSelector<
  T extends keyof RootState, 
  S extends { [selector: string]: (state: RootState[T]) => any }
>(
  scopeNamespace: T,
  selectors: S
): { [K in keyof S]: (state: RootState) => ReturnType<S[K]> } {
  return Object.keys(selectors).reduce((scoped, key) => ({
    ...scoped,
    [key]: (state: RootState) => selectors[key](state[scopeNamespace])
  }), {} as any);
}

Using{} as any删除了reduce函数中的任何类型安全性,但你一开始就没有任何类型安全性,所以我对此并不感到难过。

这是一个 StackBlitz,因此您可以看到它的实际效果:链接


推荐阅读