打字稿数组类型,其中输出是动态输入数量的所有可能组合
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
,因为编译器几乎不可能遵循输入逻辑。您可以做一些更安全的事情,但只要实现正确,就没有理由这样做。
无论如何,T
是args
的类型。输出类型是一个数组,其元素的类型为[I in keyof T]: UnwrapArray<T[I]>
。通常,T
将是一个tuple,因此输出数组元素将是一个与T
长度相同的mapped tuple;输出元组中的每个属性都将从输入元组中的相应属性转换为UnwrapArray<T[I]>
;如果T[I]
(函数的I
th 参数的类型)不是数组,则保留它;否则,您将获得数组的元素类型。
让我们看看它是否有效:
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
【讨论】:
以上是关于打字稿数组类型,其中输出是动态输入数量的所有可能组合的主要内容,如果未能解决你的问题,请参考以下文章