Given the following schema and instance:
interface SubState {
value: number
}
interface AppState {
subState: SubState
}
const state: AppState = {
subState: { value: 42 },
}
I have an nested object with functions (Redux selectors) that use that state instance or sub parts of it:
const selectors = {
subState: { isPositive: (state: SubState) => state.value > 0 },
}
The actual selector object is several layers deep and many tens of functions at different levels of the state tree.
I have transformed the selectors object into the following type (using a function shown at the end). Every key is iterated over, if it is a function then it is replaced with a function that takes the top level state AppState, finds the correct substate for the function and invokes it with it. So the signatures are all transformed from: (SpecificSubState) => ReturnType to (AppState) => ReturnType:
const mappedSelectors = {
subState: { isPositive: (state: AppState) => true },
}
I would like to have a working robust dynamic type for the return value of the mapping function. I attempted using the following implementation but it does not work yet:
interface TypedFunction<T> extends Function {
(state: T): any;
}
type RecursivePicker<Sel, State> = {
[K in keyof Sel]: Sel[K] extends TypedFunction<State>
? ((state: AppState) => ReturnType<Sel[K]>)
: (
Sel[K] extends object
? RecursivePicker<Sel[K], State>
: never
// never
)
}
const mappedSelectors: RecursivePicker<typeof selectors, AppState> = {
subState: { isPositive: (state: AppState) => true },
}
// errors with:
// Cannot invoke an expression whose type lacks a call signature.
// Type 'RecursivePicker<(state: SubState) => boolean, AppState>' has no compatible call signatures.
mappedSelectors.subState.isPositive(state)
type ExpectedTypeManual = (state: AppState) => true
type MutuallyExtends<T extends U, U extends V, V=T> = true
// errors with:
// Type 'RecursivePicker<(state: SubState) => boolean, AppState>' does not satisfy the constraint 'ExpectedTypeManual'.
// Type 'RecursivePicker<(state: SubState) => boolean, AppState>' provides no match for the signature '(state: AppState): true'.
type ShouldBeNoErrorHere = MutuallyExtends<typeof mappedSelectors.subState.isPositive, ExpectedTypeManual>
I'm not sure where the problem lies. Any advice on how to debug further please?
Link to Typescript playground with code.
Related questions:
- Typescript recursive subset of type
- TypeScript: why isn't `Function` assignable to `(...args: any[]) => any`?
Mapping function included for completeness (function works but typing is partial and not working yet):
function mapSelectors<Sel, State, SubState> (selectors: Sel, stateGetter: (state: State) => SubState) {
const mappedSelectors = Object.keys(selectors).reduce((innerMappedSelectors, selectorOrSelectorGroupName) => {
const value = selectors[selectorOrSelectorGroupName]
if (typeof value === "function") {
innerMappedSelectors[selectorOrSelectorGroupName] = (state: State) => value(stateGetter(state))
} else {
function getSubState (state: State) {
return stateGetter(state)[selectorOrSelectorGroupName]
}
innerMappedSelectors[selectorOrSelectorGroupName] = mapSelectors(value, getSubState)
}
return innerMappedSelectors
}, {} as {[selectorName in keyof typeof selectors]: (state: State) => ReturnType<typeof selectors[selectorName]>})
// }, {} as {[selectorName in keyof typeof gameSelectors]: (state: ApplicationState) => ReturnType<typeof gameSelectors[selectorName]>}),
return mappedSelectors
}