在 TypeScript 中分配箭头函数参数的并集

Distribute union of arrow function argument(s) in TypeScript

在回答问题之前,我会先列出我的整个思考过程——部分原因是搜索引擎可能会选择一些关键字,而且这样回答者就不必不必要地详细说明。

在 TypeScript 中,AB 两种类型的联合类型 U 看起来像

type U = A | B;

这意味着 U 类型的值可以是 AB.

类型

假设我想定义一个带有两个参数的箭头函数类型 Fx 类型 Xy 类型 Y) returns 类型 Z 的值。看起来像:

type F = (x: X, y: Y) => Z;   // (1)

到目前为止一切顺利。但是,现在我希望 类型 F 的函数接受 X1X2 作为它们第一个参数的类型,并且 Y1Y2作为他们第二个参数的类型。综上所述,直觉会提示

type F = (x: X1|X2, y: Y1|Y2) => Z;   // (2)

...但这是错误的! (x: X1, y: Y1) => Z 类型的值不能分配给 F 类型的值,因为上面的 TypeScript 类型 F 代表 联合类型 X1|X2 的函数] 作为第一个参数的类型,联合类型 Y1|Y2 作为第二个参数的类型 。实际上,(2) 等同于:

type Xs = X1 | X2;
type Ys = Y1 | Y2;
type F = (x: Xs, y: Ys) => Z;   // (3), equivalent to (2)

手动,我们的问题是通过写

解决的
type F = ( (x: X1, y: Y1) => Z ) | ( (x: X1, y: Y2) => Z ) | ( (x: X2, y: Y1) => Z ) | ( (x: X2, y: Y2) => Z )  // (4)

参数并集已被函数并集所取代,前一个并集的类型分布在它们之上。如果您愿意的话,它看起来有点像 TypeScript 类型的笛卡尔积。

这是我的问题:只知道可接受参数类型的联合类型(XsYs,所以我无法提前手动写出这样的笛卡尔积。回到我的第一行代码(类型 U 的定义),显然,如果只给出 U 而没有 AB,你仍然可以使用它来意思是 联合中的任何一种类型都可以。我如何指定联合中的任何一种类型都将用于箭头函数参数的类型定义?

这里有一种方法可以为具有两个参数的函数执行此操作:我假设您希望结果是标记联合,以便您可以知道在运行时传递给函数的参数类型。

为了描述每个参数的联合类型的标签,它接受像 {x1: X1, x2: X2} 这样的对象,以便 'x1''x2' 成为 X 的标签union,结果使用像 'x1-y1' 这样的标签。如果您需要保留原始标签,可以直接添加 xTag: Xi, yTag: Yi 等属性。

type CreateTaggedUnionOf2Functions<X, Y, T> = {
    [Xi in keyof X]: {
        [Yi in keyof Y]: {
            tag: `${Xi & string}-${Yi & string}`,
            func: (x: X[Xi], y: Y[Yi]) => T
        }
    }[keyof Y]
}[keyof X]


// --- Test ---
type Test = CreateTaggedUnionOf2Functions<{x1: X1, x2: X2}, {y1: Y1, y2: Y2}, Z>
/* equivalent to:
type Test = {tag: "x1-y1", func: (x: X1, y: Y1) => Z}
          | {tag: "x1-y2", func: (x: X1, y: Y2) => Z}
          | {tag: "x2-y1", func: (x: X2, y: Y1) => Z}
          | {tag: "x2-y2", func: (x: X2, y: Y2) => Z}
*/

Playground Link

我不满意编写一个专门适用于具有两个参数的函数的解决方案,所以这是一个通用版本:它相当复杂,但没有我想象的那么复杂。也许它可以以某种方式简化。

// --- Implementation ---
type CreateTaggedUnion<A extends any[]>
    = A extends [infer X]
    ? {
        [Xi in keyof X]: {tag: Xi, arr: [X[Xi]]}
    }[keyof X]
    : A extends [infer X, ...infer Y]
    ? {
        [Xi in keyof X]: CreateTagMap<Y> extends infer U ? {
            [Yi in keyof U]: {
                tag: `${Xi & string}-${Yi & string}`,
                arr: U[Yi] extends any[] ? [X[Xi], ...U[Yi]] : never
            }
        }[keyof U] : never
    }[keyof X]
    : never

type CreateTagMap<A extends any[]>
    = CreateTaggedUnion<A> extends infer U
    ? [U] extends [{tag: infer K, arr: any}]
    ? {[Ki in K & string]: (U & {tag: Ki})['arr']}
    : never
    : never

type CreateTaggedUnionOfFunctions<A extends any[], R>
    = CreateTagMap<A> extends infer U
    ? {
        [K in keyof U]: U[K] extends any[] ? {tag: K, func: (...args: U[K]) => R} : never
    }[keyof U]
    : never


// --- Tests ---
interface X1 {x: 1}
interface X2 {x: 2}
interface Y1 {y: 1}
interface Y2 {y: 2}
interface Z1 {z: 1}
interface Z2 {z: 2}
interface O {o: 3}

type TestM = CreateTagMap<[{x1: X1, x2: X2}, {y1: Y1, y2: Y2}, {z1: Z1, z2: Z2}]>
/** equivalent to TypeScript type:
 * type TestM = {
 *   "x1-y1-z1": [X1, Y1, Z1];
 *   "x1-y1-z2": [X1, Y1, Z2];
 *   "x1-y2-z1": [X1, Y2, Z1];
 *   "x1-y2-z2": [X1, Y2, Z2];
 *   "x2-y1-z1": [X2, Y1, Z1];
 *   "x2-y1-z2": [X2, Y1, Z2];
 *   "x2-y2-z1": [X2, Y2, Z1];
 *   "x2-y2-z2": [X2, Y2, Z2];
 * }
 */
type TestU = CreateTaggedUnion<[{x1: X1, x2: X2}, {y1: Y1, y2: Y2}, {z1: Z1, z2: Z2}]>
/** equivalent to TypeScript type:
 * type TestU = {
 *   tag: "x1-y1-z1";
 *   arr: [X1, Y1, Z1];
 * } | {
 *   tag: "x1-y1-z2";
 *   arr: [X1, Y1, Z2];
 * } | {
 *   tag: "x1-y2-z1";
 *   arr: [X1, Y2, Z1];
 * } | {
 *   tag: "x1-y2-z2";
 *   arr: [X1, Y2, Z2];
 * } | {
 *   tag: "x2-y1-z1";
 *   arr: [X2, Y1, Z1];
 * } | ...
 */
type TestF = CreateTaggedUnionOfFunctions<[{x1: X1, x2: X2}, {y1: Y1, y2: Y2}, {z1: Z1, z2: Z2}], O>
/** equivalent to TypeScript type:
 * type TestF = {
 *   tag: "x1-y1-z1";
 *   func: (args_0: X1, args_1: Y1, args_2: Z1) => O;
 * } | {
 *   tag: "x1-y1-z2";
 *   func: (args_0: X1, args_1: Y1, args_2: Z2) => O;
 * } | {
 *   tag: "x1-y2-z1";
 *   func: (args_0: X1, args_1: Y2, args_2: Z1) => O;
 * } | {
 *   tag: "x1-y2-z2";
 *   func: (args_0: X1, args_1: Y2, args_2: Z2) => O;
 * } | {
 *   tag: "x2-y1-z1";
 *   func: (args_0: X2, args_1: Y1, args_2: Z1) => O;
 * } | ...
 */

Playground Link