Optionally required parameters in typescript

Viewed 230

I am trying to strongly type a flux implementation (Vuex, to be more specific). My current code looks like this:

const actions = {
  first(context: Context, payload: string) { return doSomething(context, payload); },
  second(context: Context) { return doSomethingWithNoPayload(context); }
}

type Actions = typeof actions;
type PayloadType<A extends keyof Actions> = Parameters<Actions[A]>[1];

function dispatch<A extends keyof Actions>(action: A): ReturnType<Actions[A]>;
function dispatch<A extends keyof Actions>(action: A, payload: Payload<A>): ReturnType<Actions[A]>;
function dispatch<A extends keyof Actions>(action: A, payload: Payload<A> = undefined): Promise<any> {
  return Promise.resolve({ action, payload });
}

What I am trying to accomplish is:

dispatch("first") // error, no payload specified
dispatch("first", false) // error, payload type is not correct
dispatch("first", "correct") // ok, payload type is correct

dispatch("second", "something") // error, shouldn't pass payload`
dispatch("second") // ok, payload is not passed
dispatch("third") // error, non-existent action

But using an optional parameter as the payload is not enforcing me to pass the payload to the "first" action that actually needs a payload. If I don't declare the payload as optional, then I'm forced to call dispatch("second", undefined) because now I need to pass 2 parameters.

Any thoughts? Much appreciated.

1 Answers

Here's how I'd proceed assuming you need the compiler to determine the dispatch() signature programmatically from the type of actions. First, in order to get your example to compile I will guess some types, which hopefully don't affect the correctness of the solution:

// who knows
type Context = { c: string };
declare function doSomething(c: Context, payload: string): number;
declare function doSomethingWithNoPayload(c: Context): boolean;

const actions = {
  first(context: Context, payload: string) { return doSomething(context, payload); },
  second(context: Context) { return doSomethingWithNoPayload(context); }
}

type Actions = typeof actions;

TypeScript 3.0 introduced support for using tuples to represent function parameter lists. This gives us the Parameters<FuncType> type alias that returns a tuple, as you're using. But it also gives us more ability to manipulate tuples in general. Here we define Tail<T>, which takes a tuple type T and returns a new tuple with the first element removed:

// strip the first element off a tuple
// e.g., Tail<[1,2,3]> is [2,3]
type Tail<T extends readonly any[]> =
  ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;

Then we can make a single signature for dispatch() which takes one argument of type A, and a rest parameter of type Tail<Parameters<Actions[A]>>.

// use rest tuples
declare function dispatch<A extends keyof Actions>(
  action: A, ...payload: Tail<Parameters<Actions[A]>>
): ReturnType<Actions[A]>;

This should give you exactly the behavior you want:

dispatch("first") // error, no payload specified
dispatch("first", false) // error, payload type is not correct
dispatch("first", "correct") // ok, payload type is correct

dispatch("second", "something") // error, shouldn't pass payload`
dispatch("second") // ok, payload is not passed
dispatch("third") // error, non-existent action

Looks good. Hope that helps; good luck!

Link to code

Related