Reputation: 1536
I have two generic types to use with my redux actions and action creators. I am trying to conditionally make a generic function type have an optional argument based on a passed in argument. Is there a more elegant way to write GenericActionCreator
below?
import { AnyAction } from 'redux';
interface GenericAction<TType extends string, TPayload = undefined> extends AnyAction {
type: TType;
payload: TPayload;
}
// This function checks if the payload property on the passed in TAction can be undefined.
// If it can be undefined, then the payload argument becomes optional, otherwise it is
// required.
type GenericActionCreator<
TAction extends GenericAction<string, TAction['payload']>
> = Extract<TAction['payload'], undefined> extends never
? (payload: TAction['payload']) => TAction
: (payload?: TAction['payload']) => TAction;
type RequiredPayloadAction = GenericAction<string, boolean>;
type RequiredPayloadActionCreator = GenericActionCreator<RequiredPayloadAction>;
// output: type RequiredPayloadActionCreator = (payload: boolean) => RequiredPayloadAction
type OptionalPayloadAction = GenericAction<string, boolean | undefined>;
type OptionalPayloadActionCreator = GenericActionCreator<OptionalPayloadAction>;
// output: type OptionalPayloadActionCreator = (payload?: boolean | undefined) => OptionalPayloadAction
You can see that if the payload can be undefined, it makes the payload argument optional, otherwise it requires it. Is there a more efficient way to write it?
Upvotes: 0
Views: 99
Reputation: 327624
If your existing GenericActionCreator<T>
works for your use cases, it looks fine to me.
There are, of course, different ways to get the behavior you're talking about... but I don't know that any of them are more "elegant" (or at least it's somewhat a matter of opinion). Here's a version which gives the same output types as yours (note that function parameter names are not considered part of the type... so (x: string)=>void
and (y: string)=>void
are the same type) but expresses it differently:
type GenericActionCreator<T extends GenericAction<string, unknown>, P = T["payload"]> =
(...p: undefined extends P ? [P?] : [P]) => T
I'll explain each change, and note that each change can be applied or not independently of the others, so you can abandon any change you don't like:
TAction
with T
. This is only a cosmetic change.T extends GenericAction<string, T["payload"]>
is recursive but does not need to be; instead, you could replace it with T extends GenericAction<string, unknown>
, because any T
assignable to GenericAction<string, unknown>
will by necessity be assignable to GenericAction<string, T["payload"]>
T["payload"]
is being done multiple times in your version; there are different ways to avoid repeating this lookup. One is to assign it to a new generic type variable P
whose default is the value you want; that's what I've done with type GAC<T, P = T["payload"]> = ⋯
. Another way is to use conditional type inference: type GAC<T> = T["payload"] extends infer P ? ⋯: never
. (⋯) => T
, it might be possible to push the conditional type into the parameter list of the function, to avoid duplicating the function signature. There's a relationship between parameter lists and tuples we can use to do this: The function type (x: X)=>void
can also be written (...xs: [X])=>void
; the former is a function of one argument of type X
, while the latter is a function whose list of arguments is a one-element tuple of type X
. And the function type (x?: X)=>void
can also be written (...xs: [X?])=>void
; the former is a function of one optional argument of type X
, while the latter is a function whose list of arguments is a one-element optional tuple of type X
. That question mark after the type in the tuple denotes it as optional... you can think of [X?]
as similar to the union [] | [X]
.Okay, hope that helps give you some options and direction. Good luck!
Upvotes: 1