Reputation: 5777
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
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:
handleActions
branch on the brandLike 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);
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)
Upvotes: 2