【问题标题】:Infer return type based on variadic argument type根据可变参数类型推断返回类型
【发布时间】:2020-12-26 16:03:04
【问题描述】:

我正在构建一个包含大量命令和选项的大型 CLI 应用程序,其中一些在命令中重复使用。我正在使用yargs 库,我发现自己正在创建如下辅助函数:


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

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

他们像这样使用它们:

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

现在,我想创建一个帮助函数来减少嵌套,我可以这样做:

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


实现withOptions 函数相对简单,但我在输入它时遇到了困难。到目前为止,我想出了:

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 ...

但这迫使我为withOptions 的任意数量的参数编写类型。是否可以为 withOptions 创建一个单一类型签名,告诉 typescript 传入的任意数量的 YargsBuilders,返回类型将是所有这些类型的联合?

【问题讨论】:

  • 我的理解是否正确:您希望 withOptions 成为一个接受函数列表的函数 - 每个函数都以 Argv 作为输入,对其进行变异并返回它 - 并返回一个函数给定一个Argv 依次通过每个转换器函数?
  • 如果是这种情况,你为什么需要特殊的打字?
  • 为什么不使用配置对象方法而不是构建器?
  • @Aron,是的,就是这样。它需要特殊类型,因为我希望返回函数的结果返回类型是所有传入函数的返回值的联合类型
  • @Lesiak 构建器模式来自yargs,而不是来自我的代码。我无法改变它。

标签: typescript typescript-generics


【解决方案1】:

是的,这是可能的,但并不简单。您可能需要考虑按照其他人的建议以不同的方式构建代码;我对yargs 不熟悉,因此无法对此发表评论。

此解决方案使用两个一次性类型别名,您可以将它们复制并粘贴到它们的用法中以删除它们,或者根据您希望在函数中键入内容的方式以不同的方式构造它们。

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
}

测试用法:

// 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[];
// }>>

【讨论】:

  • 哇...看来我要学习更多打字魔法...这完全符合我的要求?
【解决方案2】:

为什么不将所有选项作为配置对象传递?

不同的语言支持不同的工具来提供可选参数,虽然在其他一些语言中构建器可能是你唯一的选择,但配置对象在 JS 中是更惯用的方法,yargs 也支持这一点:

.options(key, [opt])

.options() 可以选择将键映射到 opt 参数的对象。

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

所以,在你的情况下,它变得像:

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

您可以将一些选项提取到常量中:

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
});

【讨论】:

  • 你说得对,切换到配置对象更有意义,我忘记了 yargs 也提供了这一点。我仍然会接受@Mingwei Samuel 的回答,因为我的问题更多是关于打字的复杂性,我认为根据我的问题标题,人们会在这里寻找。
  • @DeX3 够公平
猜你喜欢
  • 1970-01-01
  • 2020-02-28
  • 1970-01-01
  • 1970-01-01
  • 2021-12-09
  • 2017-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多