简体   繁体   中英

Infer return type based on variadic argument type

I'm building a large CLI app with lots of commands and options, some of which are reused among commands. I'm using the yargs library and I find myself creating helper functions like the following:


const withFoo = (yargs: Argv) =>
    yargs.option('foo', {
        type: 'string',
    });

const withBar = (yargs: Argv) =>
    yargs.option('bar', {
        type: 'string',
    });

and them using them like this:

const bazOptionsBuilder = (yargs: Argv) =>
    withFoo(withBar(yargs)).option('baz', { type: 'string' });

Now, I wanted to create a helper function to reduce nesting, that I could do this instead:

const bazOptionsBuilder = withOptions(
    withFoo,
    withBar,
    /* ... possibly more here */
    (yargs) => yargs.option('baz', { type: 'string' }),
);


It's relatively trivial to implement the withOptions function, but I'm struggling with typing it. So far, I've come up with:

import { Argv } from 'yargs';
type YargsBuilder<A extends Argv> = (y: Argv) => A;
type ArgsOf<T> = T extends Argv<infer A> ? A : never;

export function withOptions<A extends Argv<any>, B extends Argv<any>>(
    a: YargsBuilder<A>,
    b: YargsBuilder<B>,
): YargsBuilder<Argv<ArgsOf<A> & ArgsOf<B>>>;
export function withOptions<A extends Argv<any>, B extends Argv<any>, C extends Argv<any>>(
    a: YargsBuilder<A>,
    b: YargsBuilder<B>,
    c: YargsBuilder<C>,
): YargsBuilder<Argv<ArgsOf<A> & ArgsOf<B> & ArgsOf<C>>>;
// ... and more ...

but this forces me to write typings for any number of arguments to withOptions . Is it possible to create a single type signature for withOptions that will say tell typescript for any number of passed in YargsBuilder s, the return type will be the union of all those types?

Why not pass all options as a configuration object?

Different languages support different tools for providing optional arguments, and while builders may be your only option in some other languages, configuration object is more idiomatic approach in JS, and yargs supports this it as well:

.options(key, [opt])

Optionally .options() can take an object that maps keys to opt parameters.

 var argv = require('yargs') .options({ 'f': { alias: 'file', demandOption: true, default: '/etc/passwd', describe: 'x marks the spot', type: 'string' } }) .argv ;

So, in your case, it becomes sth like:

yargs.options({
  'foo': {
    type: 'string'
   },
   'bar': {
     type: 'string'
   },
   'baz': {
     type: 'string'
   },
})

You can extract some options to constants:

const fooOption = {
  'foo': {
      type: 'string'
  }
} as const;

const barOption = {
  'bar': {
      type: 'string'
  }
} as const;

const bazOption = {
  'baz': {
      type: 'string'
  }
} as const;

yargs.options({
    ...fooOption,
    ...barOption,
    ...bazOption
});

Yes it is possible but not simple. You may want to consider structuring your code differently as others have suggested; I am not familiar with yargs so I can't comment on that.

This solution uses two one-off type aliases, you can just copy and paste them into their usage to remove them, or structure them differently depending on how you may want to type things within the function.

Playground Link

// https://stackoverflow.com/a/50375286/2398020
type UnionToIntersection<U> = 
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type YargsBuilder<A extends Argv> = (y: Argv) => A;

type _getYargBuildersTypeUnion<A extends YargsBuilder<any>[]> = {
    [K in keyof A]: A[K] extends YargsBuilder<Argv<infer T>> ? T : never 
}[Exclude<keyof A, keyof []>];

type _withOptionsResult<A extends YargsBuilder<any>[]> =
    YargsBuilder<Argv<UnionToIntersection<_getYargBuildersTypeUnion<A>>>>

export function withOptions<A extends YargsBuilder<any>[]>(...args: A): _withOptionsResult<A>;
export function withOptions(...args: YargsBuilder<any>[]): YargsBuilder<any> {
    return null as any; // TODO
}

Test usage:

// YargsBuilders
const A0 = (arg: Argv<{ x: string }>) => arg;
const A1 = (arg: Argv<{ five: 5 }>) => arg;
const A2 = (arg: Argv<{ ids: number[] }>) => arg;

const X = withOptions(A0, A1, A1, A2);
// const X: YargsBuilder<Argv<{
//     x: string;
// } & {
//     five: 5;
// } & {
//     ids: number[];
// }>>

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM