What I need here is to somehow declare a union of all possible combination of a given union.
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]
I want to define a method decorator which can type-safely guarantees the number of arguments of the method being decorated is exactly same as the number of arguments passed in to that 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 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
}
Same applies to all scenarios where the decorated method has less arguments then the decorator call.
The underlying reason is:
(a: SomeType) => void
is compatible with (a: any, b: any) => void
since any
can be undefined.
Then I modified FixedLengthFunction
into
type Defined = string | number | boolean | symbol | object
type FixedLengthFunction<T extends any[]> =
(...args: { [k in keyof T]: Defined }) => void
// ^^^^^^^
// changes: any -> Defined
However, it becomes "false positive" and complains that
@myDecorator('m')
c(x: number) {}
is incorrect.
This time the reason is (x: number) => void
is not compatible with (arg_0: Defined) => void
. number
is narrowed version of Defined
and narrowing the type of parameters voilates the LSP, hence the error.
The problem is:
FixedLengthFunction<['m', 'n']>
is resolved to (...args: [Defined, Defined]) => void
which is further resolved as (arg_0: Defined, arg_1: Defined) => void
.
While what I actually want is:
(...args:
[number, number] |
[string, number] |
[boolean, string]
/* ...and all other possible combinations of length 2 */
) => void
So, what I need here is the magical type Combinations
at the top of this post.
Generating such a union is a bad idea. It will grow out of control and create perf problems during compilation. You probably can do it with recursive type aliases but it is highly discouraged (ie. you can trick the compiler into doing it but it might not work in the future)
That being said I think the problem you identified is wrong. You say that the function with fewer parameter is assignable to the function with more parameter because of any
. It is not. In general typescript allow a function with fewer parameters to be assigned where a function with more parameters is expected. The function implementation will ignore the extra parameters and no harm will come of it:
let fn: (a: number) => void = function () { console.log("Don't care about your args!"); }
fn(1)// 1 is ignored but that is ok
We can enforce a strict equality of number of parameter based on the fact that tuples have a length property and the fact that we can infer the actual type of the class and extract the actual parameters from the type:
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
}