首页 > 解决方案 > 如何将类型声明为联合类型的所有可能组合?

问题描述

TL,博士;

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

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) => voidnumber是缩小版本,Defined缩小参数类型会破坏 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这篇文章顶部的神奇类型。

标签: typescriptcombinationsunion-types

解决方案


产生这样的联合是一个坏主意。它会在编译过程中失控并产生性能问题。您可能可以使用递归类型别名来执行此操作,但非常不鼓励这样做(即,您可以欺骗编译器执行此操作,但将来可能无法使用)

话虽如此,我认为您发现的问题是错误的。您说参数较少的函数可以分配给参数较多的函数,因为any. 它不是。一般来说,打字稿允许在期望具有更多参数的函数的地方分配具有较少参数的函数。函数实现将忽略额外的参数,不会有任何危害:

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

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

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

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
}

推荐阅读