打字稿数组类型,其中输出是动态输入数量的所有可能组合

Posted

技术标签:

【中文标题】打字稿数组类型,其中输出是动态输入数量的所有可能组合【英文标题】:Typescript Array type where output is all possible combinations of dynamic number of inputs 【发布时间】:2021-09-07 01:50:47 【问题描述】:

我正在尝试创建一个函数来创建所有可能的输入参数组合,同时保持输出类型。

例如:

generateSets("string") // returns [["string"]]
generateSets(1) // returns [[1]]
generateSets("string", 1) // returns [["string", 1]]
generateSets(["string", "string2"], 1) // returns [["string", 1], ["string2", 1]]
generateSets("string", [1,2]) // returns [["string", 1], ["string", 2]]    
generateSets(["string", "string2"], [1,2]) // returns [["string", 1], ["string", 2], ["string2", 1], ["string2", 2]]

基本上我想要一个动态数量的未知类型的参数(并不总是原始的),如果参数是一个数组,我想返回该参数与其他参数的所有可能组合。最重要的是,我希望返回类型与参数的顺序相匹配,例如使用字符串调用函数,然后使用数字我希望返回类型是元组数组,其中第一个元素是字符串,第二个元素是数字。

我已经尝试过这个实现,但是它强制所有输入参数都是相同的类型

export function generateArgumentSets<T>(...args: Array<T | Array<T>>): Array<Array<T>> 
    const [arg1, ...rest] = args;

    if (!arg1) return [];

    const nested = generateArgumentSets(...rest);
    if (Array.isArray(arg1))
        return arg1.flatMap((arg1El) => nested.map((nestedArgs) => [arg1El, ...nestedArgs]));
    return nested.map((nestedArgs) => [arg1, ...nestedArgs]);

我尝试了另一种方法,我想更改其他元素的类型,但首先,但这也不起作用

export function generateArgumentSets<T, T2>(...args: [T | Array<T>, ...Array<T2 | Array<T2>>]): Array<[T1, ...Array<T2>]> 
    const [arg1, arg2, ...rest] = args;
    if (!arg2) return Array.isArray(arg1) ? [arg1] : [[arg1]];

    const nested = generateArgumentSets(arg2, ...rest);
    if (Array.isArray(arg1))
        return arg1.flatMap((arg1El) => nested.map((nestedArgs) => [arg1El, ...nestedArgs]));
    return nested.map((nestedArgs) => [arg1, ...nestedArgs]);

你能指出我解决这个问题的正确方向吗?

【问题讨论】:

你问的是类型还是实现?因为据我所知,您的实现只返回空数组,至少在我测试它时是这样。您是否正在寻找使其输入数组成为cartesian product 的东西,将非数组输入视为单元素数组?但是您似乎专注于有关异构数组的编译器警告;这很容易解决,但是实现/算法似乎离题了。我应该把重点放在哪里? 我的意思是,this 你在找什么?这修复了实现和类型。如果这对你有用,我会写一个答案;如果没有,请详细说明我所缺少的。 (我可以更疯狂地使用类型,以便元组的元组变成笛卡尔积元组,但我不知道你是否真的需要) HERE'S 疯狂的元组类型;可能比它的价值更多的麻烦。除非您有其他问题,否则我可能会将所有这些都写下来作为答案。 【参考方案1】:

看来您正在实现参数的n-ary Cartesian product,所以我将调用此函数cartesianProduct 而不是generateSets

首先让我们清理一下实现(仅限 JS):

function cartesianProduct(...args) 
  if (!args.length) return [[]]; // <-- note, [[]] not []
  const [arg1, ...rest] = args;
  return (Array.isArray(arg1) ? arg1 : [arg1])
    .flatMap(e => cartesianProduct(...rest).map(x => [e, ...x]));

原始版本的最大问题是在不带参数的情况下调用 cartesianProduct() 时返回的是 [] 而不是 [[]]。但无论如何,这最终都会将所有内容都折叠到[]

让我们验证它是否有效:

const x = cartesianProduct([1, 2, 3], ["a", "b"], [true, false])
console.log(x);
/* [[1, "a", true], [1, "a", false], [1, "b", true], [1, "b", false], 
    [2, "a", true], [2, "a", false], [2, "b", true], [2, "b", false],
    [3, "a", true], [3, "a", false], [3, "b", true], [3, "b", false]]  */

看起来不错。现在我们有了一个可行的实现,让我们看看类型。


一个相对简单的打字,主要是你想要的:

type UnwrapArray<T> = T extends ReadonlyArray<infer E> ? E : T;

function cartesianProduct<T extends any[]>(
  ...args: T
): Array< [I in keyof T]: UnwrapArray<T[I]> > 
  if (!args.length) return [[]] as any; 
  const [arg1, ...rest] = args;
  return (Array.isArray(arg1) ? arg1 : [arg1])
    .flatMap(e => cartesianProduct(...rest).map(x => [e, ...x])) as any;

请注意,我在实现中的几个位置是asserting 到any,因为编译器几乎不可能遵循输入逻辑。您可以做一些更安全的事情,但只要实现正确,就没有理由这样做。

无论如何,Targs 的类型。输出类型是一个数组,其元素的类型为[I in keyof T]: UnwrapArray&lt;T[I]&gt;。通常,T 将是一个tuple,因此输出数组元素将是一个与T 长度相同的mapped tuple;输出元组中的每个属性都将从输入元组中的相应属性转换为UnwrapArray&lt;T[I]&gt;;如果T[I](函数的Ith 参数的类型)不是数组,则保留它;否则,您将获得数组的元素类型。

让我们看看它是否有效:

const x = cartesianProduct([1, 2, 3], ["a", "b"], [true, false])
// const x: [number, string, boolean][]

太棒了; x 变量的类型为 [number, string, boolean][]:三元组数组。

这对于您的目的可能已经足够了。


不过,如果你足够疯狂,你可以尝试通过将输入数组解释为输入 tuples 来尽可能多地保留类型信息,然后输出将是一个元组-元组。打字很复杂而且可能很脆弱,所以我不会解释太多,除非有人真的有兴趣了解所有部分的工作原理:

type UnwrapArray<T> = T extends ReadonlyArray<infer E> ? E : T;
type MakeArray<T> = T extends readonly any[] ? T : readonly [T];
type Concat<T> = T extends readonly [infer F, ...infer R] ? 
  [...Extract<F, readonly any[]>, ...Concat<R>] : [];

type CartesianProduct<T extends readonly any[]> =
  number extends T['length'] | 
     [K in keyof T]: MakeArray<T[K]>['length'] [number] ?
  Array< [K in keyof T]: UnwrapArray<T[K]> > : _CartProd<T>;

type _CartProd<T extends readonly any[]> =
  T extends readonly [infer F, ...infer R] ?
  [MakeArray<F>, _CartProd<R>] extends [infer AF, infer CR] ? Concat<
    [I in keyof AF]:  [J in keyof CR]: 
      [AF[I], ...Extract<CR[J], readonly any[]>] 
   > : never : [[]];

type Narrowable = string | number | boolean | symbol | object | undefined 
  | void | null | [] | readonly [] | ;

function cartesianProduct<T extends (N | N[])[], N extends Narrowable>(
  ...args: T): CartesianProduct<T> 
  if (!args.length) return [[]] as any; 
  const [arg1, ...rest] = args;
  return (Array.isArray(arg1) ? arg1 : [arg1])
    .flatMap(e => cartesianProduct(...rest).map(x => [e, ...x])) as any;

现在让我们看看结果如何:

const x = cartesianProduct([1, 2, 3], ["a", "b"], [true, false])
// const x: [[1, "a", true], [1, "a", false], [1, "b", true], 
//  [1, "b", false], [2, "a", true], [2, "a", false], [2, "b", true], 
//  [2, "b", false], [3, "a", true], [3, "a", false], [...], [...]]

编译器确切地知道x 是什么类型。所以它也知道这一点:

x.length // 12 according to the compiler
console.log(x.length) // 12

const y = x[3][2] // false according to the compiler
console.log(y) // false

相当整洁!并且可能完全矫枉过正。我会坚持使用以前更简单的类型。

Playground link to code

【讨论】:

以上是关于打字稿数组类型,其中输出是动态输入数量的所有可能组合的主要内容,如果未能解决你的问题,请参考以下文章

来自数组类型的打字稿接口

打字稿:使用函数数组输入函数,该函数返回每个函数返回类型的数组

打字稿类型检查不适用于数组过滤器

两个布尔值的打字稿元组被推断为布尔类型的数组

是否可以从数组创建打字稿类型?

带有动态键的打字稿通用函数