James
James

Reputation: 5777

Infer typescript function arguments

Given the following

const action1 = (arg1: string) => {}
const action2 = (arg1: string, arg2: {a: string, b: number}) => {}
const actions = [action1, action2]
handleActions(actions)

... elsewhere ...

const handleActions = (actions: WhatTypeIsThis[]) => {
  const [action1, action2] = actions;
  action1(/** infer string */)
  action2(/** infer string and object */)
}

How can I define the WhatTypeIsThis type in order for the action args to be inferable inside handleActions?

Is it possible to define it in such a way that actions can be any number of functions with varying argument lists?

Is it possible using generics?

My solution:

I've marked the accepted answer because it was the inspiration for my solution.

// get the types of the actions, defined in outer scope
type GET = typeof api.get;
type CREATE = typeof api.create;
...

// in controller
handleActions([api.getSomething, api.create])

...

// in service handler
const handleActions = (actions: [GET, CREATE]) => {
  const [action1, action2] = actions;
  // now we have all input / output type hints
}

This approach lets me isolate my logic from the http server, auth, and everything else I didn't write, so I can test the complexity within my service handlers in peace.

Upvotes: 1

Views: 110

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1075537

Is it possible to define it in such a way that actions can be any number of functions with varying argument lists?

With a dynamic list, you need runtime checking. I don't think you can do runtime checking on these functions without branding them (I tried doing it on length since those two functions have different lengths, but it didn't work and it really wouldn't have been useful even if it had — (arg1: string) => void and (arg1: number) => void are different functions with the same length).

With branding and a runtime check, it's possible:

  • Define branded types for the functions
  • Create the functions
  • Define the action list as an array of a union of the function types
  • Have handleActions branch on the brand

Like this:

type Action1 = (
    (arg1: string) => void
) & {
    __action__: "action1";
};

type Action2 = (
    (arg1: string, arg2: {a: string, b: number}) => void
) & {
    __action__: "action2";
};

const action1: Action1 = Object.assign(
    (arg1: string) => {},
    {__action__: "action1"} as const
);
const action2: Action2 = Object.assign(
    (arg1: string, arg2: {a: string, b: number}) => {},
    {__action__: "action2"} as const
);

const actions = [action1, action2];

type ActionsList = (Action1 | Action2)[];

const handleActions = (actions: ActionsList) => {
    const [action1, action2] = actions;
    if (action1.__action__ === "action1") {
        action1("x");   // <==== infers `(arg: string) => void` here
    }
};  

handleActions(actions);

Playground link


Before you added the text quoted at the top of the answer, it was possible with a readonly tuple type. I'm keeping this in the answer in case it's useful to others, even though it doesn't apply to your situation.

Here's what that looks like:

type ActionsList = readonly [
    (arg1: string) => void,
    (arg1: string, arg2: { a: string; b: number; }) => void
];

To make it readonly, you'll need as const on actions:

const actions = [action1, action2] as const;
//                                ^^^^^^^^^

A tuple is a kind of "...Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions." (from the link above)

Playground link

Upvotes: 2

Related Questions