I'm trying to create a higher order function which recevies an object of functions. Each function receives a state
and an action
object which has two fields (payload can be optional). The object must contain functions named as the field type
of an action:
type RequiredActionFields<P = {}> = {
type: string;
payload: P;
};
Here's the function implementation:
type SomeFunction<S, A> = (state: S, action: A) => void;
type ObjWithFunctions<S, A extends RequiredActionFields> = Record<
A["type"],
SomeFunction<S, A>
>;
const functionThatDoesThings = <S>(state: S, cb: (draft: S) => void) => {
// does domethings else
return state;
};
function createThing<S, A extends RequiredActionFields>(
functions: ObjWithFunctions<S, A>
) {
return (state: S, action: A) => {
const reducer = functions[action.type as keyof typeof functions];
if (!reducer) {
throw new Error(`Unhandled action type`);
}
const nextState = functionThatDoesThings(state, (draft) => {
reducer(draft, action);
});
return nextState;
};
}
And now I want to use this function like this:
type Action =
| { type: "something"; payload: { id: 1 } }
| { type: "another"; payload: { close: true } };
type State = {
entities: [];
open: boolean;
};
createThing<State, Action>({
something: (state, action) => {
// ERROR: I want this typing to be inferred automatically because of the object key 'something'
action.payload.id;
if (action.type === "something") {
// WORKING: like this
action.payload.id;
}
},
another: (state, action) => {
// ERROR: I want this typing to be inferred automatically
action.payload.close;
}
});
The problem I have is narrowing the type down to the function specified into the obejct of functions.
It's kind of hard to clearly explain my intent so I hope that the code shows what I'm trying to do. Here's a codesandbox to also try it out: https://codesandbox.io/s/nice-hermann-kiigqy?file=/src/index.ts:804-1325
CodePudding user response:
A mapped type will help here. Try replacing your type ObjWithFunctions = ...
definition with this:
type ObjWithFunctions<S, A extends RequiredActionFields> = {
[T in A['type']]: SomeFunction<S, A & { type: T }>
};
This creates a type with properties as follows: for each of the possible union values T
in A['type']
(i.e. T = 'something'
and T = 'another'
), create a property with key T
and value of type SomeFunction<S, A & { type: T }>
. (In other words, a more flexible version of Record
.)
The purpose of the A & { type: T }
is to select out just the "branch" of your Action
union that has the correct type
.