typescript - Asociando dos tipos juntos
Tengo algo de código aquí que implementa el patrón Command.
type Type<T> = Function & { prototype: T }
export class CommandBus<Context, Command> {
private handlers = new Map<Function, Function>()
private defaultHandler: Handler<Context, Command> = (_, command) => {
throw new Error(`No handler registered for commands of type ${command.constructor.name}`)
}
constructor(readonly target: Context) {}
handle<HandledCommand extends Command, Result>(
commandType: Type<HandledCommand>,
handler: Handler<Context, HandledCommand, Result>,
) {
this.handlers.set(commandType, handler)
return this
}
do(command: Command) {
const handler = this.handlers.get(command.constructor) || this.defaultHandler
return handler(this.target, command)
}
}
export interface Handler<Context, HandledCommand, Result = void> {
(target: Context, command: HandledCommand): Result
}
Las pruebas de esto están aquí y algunos otros ejemplos de su uso están aquí y aquí.
Soy bastante nuevo en TypeScript, y lo que no puedo averiguar es cómo modelar la asociación entre un Comando (que en esta implementación al estilo CQRS del patrón, es básicamente solo un mensaje) y el tipo de resultado esperado.
En este momento, tenemos que especificar el tipo de retorno al llamar a la función del manejador, pero siento que esto podría incorporarse en el tipo del Comando y, a partir de ahí, implicar un tipo de retorno para el Manejador).
Creo que tal vez necesite usar un tipo de tupla, de manera que un comando se defina no solo como GetRefs o lo que sea, sino como [GetRefs, Promise<Ref[]>. No estoy seguro de dónde o cómo hacer esto.
Respuesta #1
Quizás podrías hacer algo como lo siguiente:
class CommandBus<T, C extends { constructor: Function }, M extends [C, any] = never> {
private handlers = new Map<Function, Function>()
private defaultHandler: Handler<T, C> = (_, command) => {
throw new Error(`No handler registered for commands of type ${command.constructor.name}`)
}
constructor(readonly target: T) { }
handle<H extends C, R>(
commandType: Type<H>,
handler: Handler<T, H, R>,
): CommandBus<T, C, M | [H, R]> {
this.handlers.set(commandType, handler)
return this;
}
do<H extends M[0]>(command: H): Extract<M, [H, any]>[1] {
const handler = this.handlers.get(command.constructor) || this.defaultHandler
return handler(this.target, command)
}
}
He añadido un parámetro de tipo extra M a tu clase que representa el mapeo de entrada a salida como una unión de tuplas. Cuando llamas a new CommandBus(context), el valor de M se inferirá como su valor predeterminado never, lo que significa que dicho bus de comandos no puede manejar inicialmente ningún comando.
Entonces, cuando llamas a handle() en un valor de tipo CommandBus<T, C, M>, devuelves un valor de tipo CommandBus<T, C, M | [H, R]> donde H es el tipo del comando manejado y R es el tipo del resultado. Ten en cuenta que esto espera que uses encadenamiento de métodos para agregar manejadores. Por ejemplo, si llamas a const newBus=bus.handle(x, y), deberías dejar de usar bus y empezar a usar newBus, ya que solo newBus es conocido por el compilador como capaz de manejar instancias de x.
Y finalmente, cuando llamas a do() en un valor de tipo CommandBus<T, C, M>, solo te permite pasar un command de un tipo que se encuentra en M (M[0] es la unión de todos los comandos que se pueden manejar). Y el valor de retorno es el tipo de resultado análogo (Extract<M, [H, any]> extrae el único elemento de la unión de M que coincide con H, y luego Extract<M, [H, any]>[1] obtiene el tipo de resultado).
Veamos si funciona, usando cosas inventadas:
const b = new CommandBus({ a: "hello" })
.handle(Date, (t, c) => c.getFullYear() + " " + t.a)
.handle(RegExp, (t, c) => c.test(t.a));
Aquí estoy haciendo encadenamiento de métodos para que la única instancia de CommandBus que tengo con nombre sea la que ya está configurada para manejar valores de Date y RegExp. Y aquí vamos:
const d = b.do(new Date()).toUpperCase(); // string
console.log(d); // 2020 HELLO
const r = b.do(/hello/); // boolean
console.log(r); // true
Me parece bien. Bien, espero que eso ayude. ¡Buena suerte!
Enlace de Playground al código
Respuesta #2
Hasta donde entiendo, estás buscando una manera de cambiar handle de
handle<HandledCommand extends Command, Result>(
commandType: Type<HandledCommand>,
handler: HandlesCommands<Context, HandledCommand, Result>,
): CommandBus<Context, Command> {
this.handlers.set(commandType, handler)
return this
}
a
handle<HandledCommand extends Command, Result>(
handler: HandlesCommands<Context, HandledCommand, Result>,
): CommandBus<Context, Command> {
const commandType: Type<HandledCommand> = magic(handler)
this.handlers.set(commandType, handler)
return this
}
¿Es eso correcto?
Si es así, la respuesta corta es "no". Necesitas una referencia al objeto de la clase para commandType para usar en tiempo de ejecución como clave para los manejadores. Puedes obtener el tipo de commandType a partir del argumento handler, pero no puedes obtener una referencia al objeto de la clase.
La distinción es entre espacio de declaración de tipos y espacio de declaración de variables -- más sobre eso en TypeScript Deep Dive: Declaration Spaces.
Dado que no se puede obtener el objeto de clase a partir de un tipo, cualquier respuesta a tu pregunta equivaldrá a reorganizar el código de manera que el objeto de clase se pase como un parámetro de alguna otra manera, pero aún así deberá pasarse en cada llamada a handle de alguna manera.
