如何将一个类型声明为联合类型的所有可能组合?

时间:2019-02-15 05:20:07

标签: typescript combinations union-types

TL,DR;

我在这里需要以某种方式声明一个给定联合的所有可能组合的联合。

type Combinations<SomeUnion, T extends any[]> = /* Some magic */
//                          ^^^^^^^^^^^^^^^
//                       this type argument provides the information
//                       about what is the length of expected combination.

// then


Combinations<string | number, ['x', 'y']> =
    [string, string] |
    [string, number] |
    [number, string] |
    [number, number] 

Combinations<string | number | boolean, ['x', 'y']> =
    [string, string]  |
    [string, number]  |
    [string, boolean] |
    [number, string]  |
    [number, number]  |
    [number, boolean] |
    [boolean, string] |
    [boolean, number] |
    [boolean, boolean] 

Combinations<string | number, ['x', 'y', 'z']> =
    [string, string, string] |
    [string, string, number] |
    [string, number, string] |
    [string, number, number] |
    [number, string, string] |
    [number, string, number] |
    [number, number, string] |
    [number, number, number]

详细信息

我想定义一个方法装饰器,它可以类型安全地保证要装饰的方法的参数数量与传递给该装饰器的参数数量完全相同。



type FixedLengthFunction<T extends any[]> = (...args: { [k in keyof T]: any }) => void

function myDecorator<T extends any[]>(...args: T) {
    return <K extends string>(
        target: { [k in K]: FixedLengthFunction<T> },
        methodName: K,
        desc: any
    ) => {}
}


// Note: WAI => Works as intented
class Foo {
   @myDecorator()
   a() {}
   // expected to be correct,
   // and actually passes the type system.
   // WAI

   @myDecorator()
   b(x: number) {}
   // expected to be incorrect since 'b' has one more argument,
   // and actually catched by the type system.
   // WAI

   @myDecorator('m')
   c(x: number) {}
   // expected to be correct,
   // and actually passes the type system.
   // WAI

   @myDecorator('m')
   d() {}
   // expected to be incorrect since 'd' has one less argument,
   // but still passes the type system.
   // not WAI
}

对于装饰方法的参数比装饰器调用的参数少的所有情况都相同。

根本原因是: (a: SomeType) => void(a: any, b: any) => void兼容,因为any可以不确定。

然后我将FixedLengthFunction修改为

type Defined = string | number | boolean | symbol | object
type FixedLengthFunction<T extends any[]> =
    (...args: { [k in keyof T]: Defined }) => void
//                              ^^^^^^^
//                      changes: any -> Defined

但是,它变成“假阳性”并抱怨

@myDecorator('m')
c(x: number) {}

不正确。

这次的原因是(x: number) => void(arg_0: Defined) => void不兼容。 numberDefined的缩窄版本,而变窄的参数类型则使LSP无效,因此出错。

问题是: FixedLengthFunction<['m', 'n']>被解析为(...args: [Defined, Defined]) => void,其进一步被解析为(arg_0: Defined, arg_1: Defined) => void

我真正想要的是:

(...args: 
    [number, number] |
    [string, number] |
    [boolean, string] 
    /* ...and all other possible combinations of length 2 */
) => void

所以,我需要的是本文顶部的神奇类型Combinations

1 个答案:

答案 0 :(得分:1)

生成这样的工会是一个坏主意。它将失去控制并在编译过程中产生性能问题。您可能可以使用递归类型别名来做到这一点,但是强烈建议不要这样做(即,您可以欺骗编译器来做到这一点,但将来可能无法使用)

话虽如此,我认为您发现的问题是错误的。您说由于any,具有较少参数的函数可分配给具有较多参数的函数。它不是。一般而言,打字稿允许在需要具有更多参数的功能的情况下分配具有较少参数的功能。函数实现将忽略多余的参数,不会有任何危害:

let fn: (a: number) => void = function () { console.log("Don't care about your args!"); }

fn(1)// 1 is ignored but that is ok 

我们可以基于元组具有length属性的事实以及我们可以推断类的实际类型并从该类型中提取实际参数的事实来强制执行参数数量的严格相等性。

type FixedLengthFunction<T extends any[]> = (...args: { [k in keyof T]: any }) => void

type ErrorIfDifferentLength<TMethod, TExpected extends any[]> = 
    TMethod extends (...a: infer TParams) => any ? 
    TParams['length'] extends TExpected['length'] ? {}: { "!Error": "Number of parameters differ:", actual:  TParams['length'], expected: TExpected['length'] } : {}

function myDecorator<T extends any[]>(...a: T) {
    return <K extends string, TClass extends Record<K, FixedLengthFunction<T>>>(target: TClass & ErrorIfDifferentLength<TClass[K], T>, key: K): void => {

    }
}

// Note: WAI => Works as intented
class Foo {
    @myDecorator()
    a() {}
    // expected to be correct,
    // and actually passes the type system.
    // WAI

    @myDecorator()
    b(x: number) {}
    // expected to be incorrect since 'b' has one more argument,
    // and actually catched by the type system.
    // WAI

    @myDecorator('m')
    c(x: number) {}
    // expected to be correct,
    // and actually passes the type system.
    // WAI

    @myDecorator('m')
    d() {}
    // Argument of type 'Foo' is not assignable to parameter of type 'Foo & { "!Error": "Number of parameters differ:"; method: "d"; actual: 0; expected: 1; }'.
    // WAI
}