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:
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
}
So, the type of mappedSelectors
displays unhelpfully as
const mappedSelectors: RecursivePicker<{
subState: {
isPositive: (state: SubState) => boolean;
};
}, AppState>
One technique I've been using when I have a complicated type whose IntelliSense information is opaque and full of other type names is to convince the compiler to expand out the property names with the following type alias:
type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
That doesn't really change the type much (there might be edge cases, but it mostly just forces the compiler to walk through the properties of the object and write them out) but it usually improves the readability of the type.
Let's modify RecursivePicker
to use it:
type RecursivePicker<Sel, State> = Prettify<
{
[K in keyof Sel]: Sel[K] extends TypedFunction<State>
? ((state: AppState) => ReturnType<Sel[K]>)
: (Sel[K] extends object ? RecursivePicker<Sel[K], State> : never)
// never
}
>;
Now we look at mappedSelectors
and see:
const mappedSelectors: {
subState: {
isPositive: {};
};
}
Now that is a transparent type, and transparently not what you want. Why not? Let's clear away all the other stuff and see what happens with isPositive
:
type IsPositive = RecursivePicker<
{ isPositive: (x: SubState) => boolean },
AppState
>;
// type IsPositive = { isPositive: {}; }
Sure enough, it becomes { isPositive: {} }
. That means the (x: SubState) => boolean
property of doesn't extend TypedFunction<State>
where State
is AppState
. That is, (x: Substate) => boolean
doesn't extend (x: AppState) => any
. Maybe you wanted, instead of RecursivePicker<..., AppState>
, to use RecursivePicker<..., SubState>
?
const mappedSelectors: RecursivePicker<typeof selectors, SubState> = {
subState: { isPositive: (state: AppState) => true }
};
// const mappedSelectors: {
// subState: {
// isPositive: (state: AppState) => boolean;
// };
// }
Now that looks a lot like the type you wanted, and you can invoke it:
mappedSelectors.subState.isPositive(state); // okay
Your expected type is still not exactly right, because the compiler sees the return value as boolean
and not true
. As long as boolean
is all right, the following works:
type ExpectedTypeManual = (state: AppState) => boolean;
type MutuallyExtends<T extends U, U extends V, V = T> = true;
type ShouldBeNoErrorHere = MutuallyExtends<
typeof mappedSelectors.subState.isPositive,
ExpectedTypeManual
>; // okay
That's probably your main issue.
So why did you get {}
before? If a property is a function that doesn't match the type you expected, it goes on to the next clause, which is Sel[K] extends object ? ...
Sel[K] extends object ? ...
. But functions do extend object
(they are not primitives):
type FunctionExtendsObject = (() => boolean) extends object ? true : false; // true
So the (state: SubState) => boolean
gets mapped over, and it doesn't have any mappable properties (it does have properties but TypeScript ignores them when you examine the type)
type NoMappableKeys = keyof (() => boolean); // never
So you end up with {}
coming out.
What do you want to do when an object has a function property that does not match your intended type? Should it be unmodified? Should the property be removed? Should you get a compile error? Should it be changed to something completely different? You need to decide that and then possibly modify RecursivePicker
. But I think the main question has been answered, so I'll stop there.
Hope that helps; good luck!
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.