如何使用TypeScript键入对象函数值的映射?

时间:2019-06-30 18:25:51

标签: typescript

我试图通过组成reducer在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's mapnpm-ramda一起使用。 几乎起作用,除了任何选择器的返回结果是其“作用域”内所有选择器的并集。

我有set up a project on StackBlitz可以说明问题。

2 个答案:

答案 0 :(得分:4)

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);
}

使用{} as any会删除reduce函数中的所有类型安全性,但是您一开始没有任何类型安全性,因此我对此并不感到遗憾。

这是一个StackBlitz,因此您可以看到它的运行情况:Link

答案 1 :(得分:2)

是的,可以!

类型定义

请考虑以下定义:

declare function scopeSelector<Namespace extends keyof RootState, SubSelectors extends Selectors<any, RootState[Namespace]>>(scope: Namespace, selectors: SubSelectors): Result<SubSelectors>;

位置:

type Selectors<K extends string = string, S = any> = {
  [index in K]: (state: S) => ValueOf<S>;
}

type Result<T> = {
  [K in keyof T]: (state: RootState) =>
    T[K] extends AnyFunction
      ? ReturnType<T[K]>
      : never;
}

type AnyFunction = (...args: any[]) => any;
type ValueOf<T> = T[keyof T];

实施

function scopeSelector<Namespace extends keyof RootState, SubSelectors extends Selectors<any, RootState[Namespace]>>(scope: Namespace, selectors: SubSelectors): Result<SubSelectors> {
  return Object.keys(selectors)
    .reduce<Result<SubSelectors>>(
      (accumulator, current) => ({
        ...accumulator,
        [current]: (state: RootState) => selectors[current](state[scope])
      }),
      {} as Result<SubSelectors>
    )
}