Reputation: 108
I am trying to set up state in a monorepo application by composing reducers. Currently, all state is divided by domain, e.g.
type State = { fruit: FruitState, snacks: SnackState })
Each state domain contains some selectors. These selectors are defined in an encapsulated manner, e.g.
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, e.g.
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, e.g.
// 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.
Upvotes: 1
Views: 508
Reputation: 25850
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>
)
}
Upvotes: 2
Reputation: 9242
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
Upvotes: 4