Reputation: 2088
I'm trying to create a function to generate my redux action creators.
Let's say we have these types for our actions:
export type DeleteAction = {
type: typeof BLAH_DELETE;
payload: {
id: string;
};
};
export type EditAction = {
type: typeof BLAH_EDIT;
payload: {
id: string;
name: string;
};
};
export MyAction = DeleteAction | EditAction
Now in the actions file, I would like to create my actions in this way:
export const deleteBlah = makeActionCreator<MyAction>('BLAH_DELETE');
// Expected Behaviour
deleteBlah({ id: '' }) // Correct
deleteBlah({ id: '', name: '' }) // Error
export const editBlah = makeActionCreator<MyAction>('BLAH_EDIT');
// Expected Behaviour
editBlah({ id: '', name: '' }) // Correct
editBlah({ id: '' }) // Error
Here is the makeActionCreator
function:
export const makeActionCreator = <A extends { type: string; payload: any }>(type: A['type']) => (
payload: ActionPayload<ExtractAction<A, A['type']>>,
) => ({
type,
payload,
});
type ExtractAction<A, T> = A extends { type: T } ? A : never;
type ActionPayload<T extends { payload: any }> = Pick<T['payload'], keyof T['payload']>;
The problem is I don't know how can I pass the action type which is provided in actions file to the ExtractAction<A, A['type']>
so, the payload is always valid for all possible options of A
.
Upvotes: 0
Views: 307
Reputation: 17504
Eventually, I have some idea to turn your idea work. Here are the few steps:
payload
property as well:type ValueType<T, K> = K extends keyof T ? T[K] : never;
type ExtractPayload<A, T> = A extends { type: T, payload: infer R } ? R : never;
Curry
function which receives action as argument:type Curry<A> = <T extends ValueType<A, 'type'>>(arg: T) => (payload: ExtractPayload<A, T>) => {
type: T
payload: ExtractPayload<A, T>
};
makeActionCreator<MyAction>()
which is a bit annoying though:export const makeActionCreator = <A>(): Curry<A> => action => payload => ({
type: action,
payload,
})
// Testing
const deleteBlah = makeActionCreator<MyAction>()('BLAH_DELETE');
deleteBlah({ id: '' }) // Correct
deleteBlah({ id: '', name: '' }) // Error
Another solution
Another options, you would keep your function without creating more curry level but you have to pass one more typing argument as following:
type Curry1<A, T> = (payload: ExtractPayload<A, T>) => {
type: T
payload: ExtractPayload<A, T>
};
// The function is the same but have type T as new parameter
export const makeActionCreator = <A, T>(action: T): Curry1<A, T> => payload => ({
type: action,
payload,
})
// Testing, it's a bit odd as specify 'BLAH_DELETE' twice
const deleteBlah = makeActionCreator<MyAction, 'BLAH_DELETE'>('BLAH_DELETE');
deleteBlah({ id: '' }) // Correct
deleteBlah({ id: '', name: '' }) // Error
Upvotes: 1
Reputation: 1042
type Some<U, I> = (U extends I ? true : false) extends false ? false : true;
function makeCreator<T extends MyAction["type"]>(type: T) {
return function <P>(
p: { type: T; payload: P } extends MyAction
? Some<MyAction, { type: T; payload: P }> extends true
? P
: never
: never
): MyAction {
return null as any;
};
}
code
{ type: T; payload: P } extends MyAction
? OneOf<MyAction, { type: T; payload: P }> extends true
? P
: never
: never`
assert the argument you provided is exactly the payload type according type
given
Upvotes: 0