Tunn
Tunn

Reputation: 1536

Make function type have conditional optional argument

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

Answers (1)

jcalz
jcalz

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:

  • I've replaced 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"]>
  • The lookup 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.
  • Since both branches of your conditional type look like (⋯) => 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!

Playground link to code

Upvotes: 1

Related Questions