One approach is to make Middleware
generic in a type parameter T
corresponding to the tuple type of instance types constructed by the class
property of the middleware
array. So if you call Middleware([{funcs: [], class: Class1}, {funcs: [], class: Class2}])
, where Class1
and Class2
are class constructors of types named Class1
and Class2
respectively, then T
would be the type [Class1, Class2]
.
And we'd make the middleware
parameter be of a mapped type over T
, such that each element of middleware
takes on a type related to the corresponding element of T
. For each numeric index I
from the keys of T
, the class
property should be a construct signature for T[I]
(the element of T
at key I
), while the funcs
property should be an array of keys of T[I]
whose properties are functions.
We can write it like this:
function Middleware<T extends readonly object[]>(
middleware: [...{ [I in keyof T]: {
funcs: ReadonlyArray<KeysMatching<T[I], Function>>
class: new () => T[I]
} }]
) {}
where KeysMatching<T, V>
is a utility type to get the property keys of T
whose property values are assignable to V
. One possible definition is
type KeysMatching<T, V> = keyof
{ [K in keyof T as T[K] extends V ? K : never]: any }
See In TypeScript, how to get the keys of an object type whose values are of a given type? for more information about how KeysMatching
works.
Oh, and the middleware
parameter's type is wrapped in a variadic tuple type like [...
+]
to give the compiler a hint that we want T
to be inferred as a tuple and not an unordered array.
Okay, let's test it:
class TestValidation { Test() { } }
class FilterValidation { Filter() { } }
Middleware([
{ funcs: ['Test'], class: TestValidation },
{ funcs: ['Filter'], class: FilterValidation }
]); // okay
Looks good. Let's test it on an example that fails:
class Foo {
a = 1;
b = 2;
c() { }
}
Middleware([{
class: Foo, funcs: ["a", "b", "c"]; // error!
// ---------------> ~~~ ~~~
}])
That fails because "a"
and "b"
are keys of Foo
whose values are not functions. Only "c"
is accepted. So this solution works for both accepting desirable inputs and rejecting undesirable ones.
Playground link to code