Generic functions in TypeScript act as a function representing every possible specification of its generic type parameters, since it's the caller of the function that specifies the type parameter, not the implementer:
type GenericFunction = <T>(x: T) => T;
const cantDoThis: GenericFunction = (x: string) => x.toUpperCase(); // error!
// doesn't work for every T
cantDoThis({a: "oops"}); // caller chooses {a: string}: runtime error
const mustDoThis: GenericFunction = x => x; // okay, verifiably works for every T
mustDoThis({a: "okay"}); // okay, caller chooses {a: string}
So, let's look at CommandBus
:
interface CommandBus {
execute: <C extends Command, R extends Response<C>>(command: C) => Promise<R>;
}
The execute()
method of CommandBus
is a generic function that claims to be able to accept a command
of any subtype of Command
the caller wants (okay so far probably), and return a value of Promise<R>
, where R
is any subtype of Response<C>
that the caller wants. That doesn't seem to be something anyone could plausibly implement, and presumably you'll always have to assert that the response you're returning is the R
the caller asked for. I doubt this is your intent. Instead, how about something like this:
interface CommandBus {
execute: <C extends Command>(command: C) => Promise<Response<C>>;
}
Here, execute()
only has one generic parameter, C
, corresponding to the type of the passed-in command
. And the return value is just Promise<Response<C>>
, not some subtype that the caller is asking for. This is more plausibly implementable, as long as you have some way of guaranteeing that you have an appropriate handler for every C
(say, by throw
ing if you don't.)
That leads us to your Handler
interface:
interface Handler<C extends Command, R extends Response<C>> {
canHandle: (command: C) => boolean;
handle: (command: C) => Promise<R>;
}
Even if we release ourselves from the tyranny of trying to represent the particular subtype of Response<C>
a handler will produce, like this:
interface Handler<C extends Command> {
canHandle: (command: C) => boolean;
handle: (command: C) => Promise<Response<C>>;
}
we still have a problem with canHandle()
. And that's the fact that Handler
is itself a generic type. The difference between generic functions and generic types has to do with who gets to specify the type parameter. For functions, it's the caller. For types, it's the implementer:
type GenericType<T> = (x: T) => T;
const cantDoThis: GenericType = (x: string) => x.toUpperCase(); // error! no such type
const mustDoThis: GenericType<string> = x => x.toUpperCase(); // okay, T is specified
mustDoThis({ a: "oops" }); // error! doesn't accept `{a: string}`
mustDoThis("okay");
You want Handler<C>
to only handle()
a command of type C
, which is fine. But its canHandle()
method also demands that the command be of type C
, which is too strict. You want the caller of canHandle()
to choose the C
, and have the return value be true
or false
depending on whetehr the C
chosen by the caller matches the one chosen by the implementer. To represent this in the type system, I'd suggest making canHandle()
a generic user-defined type guard method of a parent interface that isn't generic at all, like this:
interface SomeHandler {
canHandle: <C extends Command>(command: C) => this is Handler<C>;
}
interface Handler<C extends Command> extends SomeHandler {
handle: (command: C) => Promise<Response<C>>;
}
So, if you have a SomeHandler
, all you can do is call canHandle()
. If you pass it a command of type C
, and canHandle()
returns true
, the compiler will understand that your handler is a Handler<C>
and you can call it. Like this:
function testHandler<C extends Command>(handler: SomeHandler, command: C) {
handler.handle(command); // error! no handle method known yet
if (handler.canHandle(command)) {
handler.handle(command); // okay!
}
}
We're almost done. The only fiddly bit is that you're using SomeHandler[]
's find()
method to locate one appropriate for command
. The compiler cannot peer into the callback handler => handler.canHandle(command)
and deduce that the callback is of type (handler: SomeHandler) => handler is SomeHandler<C>
, so we have to help it out by annotating it as such. Then the compiler will understand that the return value of find()
is Handler<C> | undefined
:
class AppCommandBus implements CommandBus {
private readonly handlers: SomeHandler[] = [];
public async execute<C extends Command>(
command: C
): Promise<Response<C>> {
const resolvedHandler = this.handlers.find((handler): handler is Handler<C> =>
handler.canHandle(command)
);
if (!resolvedHandler) throw new Error();
return resolvedHandler.handle(command);
}
}
This works pretty well with the type system and is as nice as I can make it. It may or may not work for your actual use case, but hopefully it gives you some ideas about how generics can be used effectively. Good luck!
Playground link to code
bus.execute<SomeCommand, SomeResponse>()
, it has no way of ensuring that the implementation doesn't in fact returnSomeOtherResponse
, which would both be legal based on my type signature. – Extrados