1

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.

2
  • what do you pass into your actions in the handleActions function? I assume, that your function is more something like this: (actions: DesiredType[], ...params: any) => {}, right? Commented Nov 18, 2021 at 7:32
  • 1
    @DenisLoh "what do you pass into your actions in the handleActions function?" - I pass the specific arguments that each action takes. Args can be a string, an object full of specific keys, both, and more. Commented Nov 18, 2021 at 7:41

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

4 Comments

Sorry, my question was lacking specifics - I've revised - TLDR: actions is a variable length list of different functions
@James - If the list is dynamic (contents determined at runtime), then you need dynamic checks at runtime. I think you can only do it if you brand the functions. I've updated the answer. :-) (I don't understand the downvote on the question, BTW, I think it's a reasonable question, and an interesting one.)
@T.J.Crowder I didn't downvote the question, but perhaps the reason it was downvoted is because the OP seems not to want to use a tuple type, but hasn't written a minimal reproducible example where a tuple type wouldn't be the solution, so it's hard to know what they actually want.
@kaya3 - Fair enough. :-) And I think it was done soon after the change that invalidated my original answer, which might also be a factor.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.