Summary:
In this scenario, I am looking to establish a comprehensive union of all possible combinations within a given set.
type Combinations<SomeUnion, T extends any[]> = /* Utilizing some mystical method */
// ^^^^^^^^^^^^^^^
// specifying the length of expected combinations.
// 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]
Details:
The aim is to create a method decorator that can guarantee, in a type-safe manner, that the number of arguments required by the decorated method matches the number of arguments provided to the decorator.
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 intended
class Foo {
@myDecorator()
a() {}
// Expected to be correct,
// and indeed passes the type system.
// WAI
@myDecorator()
b(x: number) {}
// Expected to be incorrect since 'b' requires one more argument,
// and detected by the type system.
// WAI
@myDecorator('m')
c(x: number) {}
// Expected to be correct,
// and successfully passes the type system.
// WAI
@myDecorator('m')
d() {}
// Expected to be incorrect since 'd' demands one less argument,
// but still passes the type system.
// not WAI
}
This applies universally when the decorated method has fewer arguments than the decorator call.
The main issue lies in the fact that:
(a: SomeType) => void
aligns with (a: any, b: any) => void
since any
can also be undefined.
I proceeded to update FixedLengthFunction
to:
type Defined = string | number | boolean | symbol | object
type FixedLengthFunction<T extends any[]> =
(...args: { [k in keyof T]: Defined }) => void
// ^^^^^^^
// changes 'any' to 'Defined'
However, it resulted in a "false positive" complaint when:
@myDecorator('m')
c(x: number) {}
was flagged as incorrect.
This time, the issue was that (x: number) => void
does not match (arg_0: Defined) => void
. number
represents a narrowed subset of Defined
, violating the Liskov Substitution Principle (LSP).
The crux of the problem is:
FixedLengthFunction<['m', 'n']>
converts to (...args: [Defined, Defined]) => void
, which further clarifies as (arg_0: Defined, arg_1: Defined) => void
.
What is truly needed is:
(...args:
[number, number] |
[string, number] |
[boolean, string]
/* ...and so forth for all possible length 2 combinations */
) => void
Hence, the essential requirement here is the magical type Combinations
featured at the beginning of this post.