简体   繁体   中英

How can I type a mapping of Object function values with TypeScript?

I am trying to set up state in a monorepo application by composing reducers. Currently, all state is divided by domain, eg

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

Each state domain contains some selectors. These selectors are defined in an encapsulated manner, eg

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

We then have a web module which imports all the state domain selectors, grouping them by key and then wrapping them in a higher-order function to scope them into domain namespaces, eg

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

This code works at runtime - but produces TypeScript errors, eg

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

I have tried using Ramda's map with the advanced typings from npm-ramda . This nearly worked, except the return result of any selector was a union of all selectors within its "scope".

I have set up a project on StackBlitz which demonstrates the problem.

TypeScript can't infer the return type of { [K in keyof typeof selector]: (state: RootState) => ReturnType<typeof selector[K]> } . Unfortunately inferring any types (or even defining them without requiring a cast) when using reduce to build an object is almost impossible.

That said, you can get the desired behavior with a bit of casting, and declaring the return type manually.

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 removes any type safety in your reduce function, but you didn't have any in the first place, so I don't feel to bad about it.

Here's a StackBlitz so you can see it in action: Link

Yes, you can!

Type definition

Consider the following definition:

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

Where:

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];

Implementation

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM