One approach that I would take is to create an AcceptableHandler<T>
utility type that transforms T
into a version where each property present becomes a zero-argument function.
type AcceptableHandler<T> =
HasProp<PickByValue<T, () => any>, { needsAtLeastOneZeroArgMethod(): any }>
To explain further, an AcceptableHandler<T>
firstly selects properties from T
that are zero-argument functions using PickByValue<T, ()=>any>
, which works similar to the Pick<T, K>
utility type but based on property values instead of keys. Then it undergoes the check for presence of at least one property with
HasProp<⋯, {needsAtLeastOneZeroArgMethod(): any}>
. If there's even one property, nothing happens; otherwise, it gets replaced with
{needsAtLeastOneZeroArgMethod(): any}
, potentially triggering error messages if there are no acceptable arguments for
send
.
We also need definitions for PickByValue<T, V>
and HasProp<T, D>
:
type PickByValue<T, V> =
{ [K in keyof T as T[K] extends V ? K : never]: T[K] }
type HasProp<T, D = never> =
T extends (keyof T extends never ? never : unknown) ? T : D
The first type remaps keys to filter out property keys, while the second type checks for the absence of known keys using a conditional type.
Using this information, we can define Handler<T>
as follows:
class Handler<T extends AcceptableHandler<T>> {
public send<P extends keyof AcceptableHandler<T>>(
methodName: P,
payload: T[P],
) {
//
}
}
Initially, we constrain T
to be of type AcceptableHandler<T>
, ensuring that T
includes at least one property suitable as a zero-argument function:
const handlerA = new Handler<Works>(); // okay
const handlerB = new Handler<DoesntWork>(); // error
// ------------------------> ~~~~~~~~~~
// Type 'DoesntWork' does not satisfy the
// constraint '{ needsAtLeastOneZeroArgMethod(): any; }'.
const handlerC = new Handler<ShouldWork>(); // okay
Next, we restrict the type parameter P
of send
to keys found in AcceptableHandler<T>
, validating only zero-arg method properties:
handlerA.send('call', () => null); // okay
handlerB.send('value', 23); // error
// ---------> ~~~~~~~
// Argument of type '"value"' is not assignable to
// parameter of type '"needsAtLeastOneZeroArgMethod"'.
handlerC.send('call', () => null); // okay
handlerC.send('value', 42); // error
// ---------> ~~~~~~~
// Argument of type '"value"' is not assignable to
// parameter of type '"call"'.
It's worth noting that the error on handlerB
might seem odd since expecting "value"
to be
"needsAtLeastOneZeroArgMethod"</code could lead to runtime issues if wrongly used. However, given that creating <code>handlerB
itself results in an error, subsequent errors are less critical. Resolving the initial error at
new Handler<DoesntWork>()
should eliminate or provide a more informative error message for the issue at
send()
.
Visit Playground link for code