Let's say I have a function that takes two functions f
and g
as arguments and returns a function that executes f
and g
and returns an object with the results. I also want to enforce that f
and g
have the same signature. This is easy enough with conditional types:
type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
function functionPair<
F extends (...args: any[]) => any,
G extends (...args: ArgumentTypes<F>) => any
>
(f: F, g: G): (...args: ArgumentTypes<F>) => { f: ReturnType<F>, g: ReturnType<G> }
{
return (...args: ArgumentTypes<F>) => ({ f: f(...args), g: g(...args) });
}
functionPair((foo: string) => foo, (bar: number) => bar); // Error, incompatible signatures, as expected
functionPair((foo: string) => foo, (bar: string) => bar.length); // (foo: string) => { f: string; g: number; }, as expected
Now, what if I want to make f
and g
optional , and have the shape of the returned object change as a result? That is, if f
or g
is undefined
, their key should be missing from the resulting object:
functionPair(); // Should be () => {}
functionPair(undefined, undefined); // Should be () => {}
functionPair((foo: string) => foo); // Should be (foo: string) => { f: string }
functionPair(undefined, (bar: string) => foo.length); // Should be (bar: string) => { g: number }
functionPair((foo: string) => foo, (bar: string) => foo.length); // Should be (foo: string) => { f: string, g: number }, as before
I've been trying to accomplish this with conditional types, but I'm having some trouble with conditionally enforcing the shape of the resulting function. Here's what I have so far (strict null checks are off):
function functionPair<
A extends F extends undefined ? G extends undefined ? [] : ArgumentTypes<G> : ArgumentTypes<F>,
F extends (...args: any[]) => any = undefined,
G extends F extends undefined ? (...args: any[]) => any : (...args: ArgumentTypes<F>) => any = undefined
>
(f?: F, g?: G): (...args: A) =>
F extends undefined
? G extends undefined ? {} : { g: ReturnType<G> }
: G extends undefined ? { f: ReturnType<F> } : { f: ReturnType<F>, g: ReturnType<G> }
{ /* implementation... */ }
const a = functionPair(); // () => {}, as expected
const b = functionPair((foo: string) => foo); // (foo: string) => { f: string; }, as expected
const c = functionPair((foo: string) => foo, (bar: number) => bar); // Error, incompatible signatures, as expected
const d = functionPair((foo: string) => foo, (bar: string) => bar.length); // (foo: string) => { f: string; g: number; }, as expected
const e = functionPair(undefined, undefined); // INCORRECT! Expected () => {}, got (...args: unknown[] | []) => {} | { f: any; } | { g: any; } | { f: any; g: any; }
const f = functionPair(undefined, (bar: string) => bar.length); // INCORRECT! Expected (bar: string) => { g: number; } but got (...args: unknown[] | [string]) => { g: number; } | { f: any; g: number; }
By the way, I know that this is technically possible with overloads, as below, but I'd really like to understand how to do it without them.
function functionPairOverloaded(): () => {}
function functionPairOverloaded(f: undefined, g: undefined): () => {}
function functionPairOverloaded<F extends (...args: any[]) => any>(f: F): (...args: ArgumentTypes<F>) => { f: ReturnType<F> }
function functionPairOverloaded<G extends (...args: any[]) => any>(f: undefined, g: G): (...args: ArgumentTypes<G>) => { g: ReturnType<G> }
function functionPairOverloaded<F extends (...args: any[]) => any, G extends (...args: ArgumentTypes<F>) => any>(f: F, g: G): (...args: ArgumentTypes<F>) => { f: ReturnType<F>, g: ReturnType<G> }
function functionPairOverloaded<F extends (...args: any[]) => any, G extends (...args: any[]) => any>(f?: F, g?: G) { /* implementation... */ }
Assuming you have --strictNullChecks
turned on, I guess I'd do it this way:
type Fun = (...args: any[]) => any;
type FunFrom<F, G> = F extends Fun ? F : G extends Fun ? G : () => {};
type IfFun<F, T> = F extends Fun ? T : never;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never
declare function functionPair<
F extends Fun | undefined = undefined,
G extends ((...args: (F extends Fun ? Parameters<F> : any[])) => any)
| undefined = undefined
>(
f?: F,
g?: G
): (...args: Parameters<FunFrom<F, G>>) => {
[K in IfFun<F, 'f'> | IfFun<G, 'g'>]: K extends 'f' ? Ret<F> : Ret<G>
};
That's fairly ugly, but it does give you the behavior you're looking for:
const a = functionPair(); // () => {}, as expected
const b = functionPair((foo: string) => foo); // (foo: string) => { f: string; }, as expected
const c = functionPair((foo: string) => foo, (bar: number) => bar); // Error, incompatible signatures, as expected
const d = functionPair((foo: string) => foo, (bar: string) => bar.length); // (foo: string) => { f: string; g: number; }, as expected
const e = functionPair(undefined, undefined); // () => {}, as expected
const f = functionPair(undefined, (bar: string) => bar.length); // (bar: string) => { g: number; }, as expected
I decided to go with just two type parameters F
and G
and instead of A
use Parameters<FunFrom<F, G>>
. Note that Parameters
is a built-in type function similar to your ArgumentTypes
.
Also, for the return type of the returned function I do a somewhat ugly mapped type. I first was planning to do something like IfFun<F, {f: Ret<F>}> & IfFun<G, {g: Ret<G>}>
, which is (I believe) more understandable, but the resulting type {f: X, g: Y}
is nicer than the intersection {f: X} & {g: Y}
.
Anyway, hope that helps. Good luck!
If you want to be able to turn --strictNullChecks
off, then the definitions get even hairier:
type Fun = (...args: any[]) => any;
type AsFun<F> = [F] extends [Fun] ? F : never
type FunFrom<F, G> = AsFun<IfFun<F, F, IfFun<G, G, () => {}>>>;
type IfFun<F, Y, N=never> = F extends undefined ? N :
0 extends (1 & F) ? N : F extends Fun ? Y : N;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never
declare function functionPair<
F extends Fun | undefined = undefined,
G extends ((...args: IfFun<F, Parameters<F>, any[]>) => any)
| undefined = undefined
>(
f?: F,
g?: G
): (...args: Parameters<FunFrom<F, G>>) => {
[K in IfFun<F, 'f'> | IfFun<G, 'g'>]: K extends 'f' ? Ret<F> : Ret<G>
};
The difference is that IfFun<>
needs to be able to distinguish functions from undefined
and any
, both of which pop up in unfortunate places when you turn off --strictNullChecks
. That's because undefined extends Function ? true : false
undefined extends Function ? true : false
starts returning true
, and any
starts getting inferred when you pass the manual undefined
value into the functions. Distinguishing undefined
is reasonably straightforward since Function extends undefined ? true : false
Function extends undefined ? true : false
is still false
, but distinguishing any
is annoying and involves some funny business .
Good luck again!
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.