typescript - Asocierea a două tipuri împreună


Am aici niște cod care implementează șablonul 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
}

Testele pentru acesta sunt aici și câteva alte exemple ale utilizării sale sunt aici și aici.

Sunt destul de nou în TypeScript, iar lucrul pe care nu reușesc să-l înțeleg este cum să modelezi asocierea dintre o Comandă (care în această implementare de tip CQRS a pattern-ului este practic doar un mesaj) și tipul rezultat așteptat.

În acest moment, trebuie să specificăm tipul de returnare în timp ce apelăm funcția handler, dar simt că acest lucru ar putea fi integrat în tipul Comenzii și de acolo să implice un tip de returnare din Handler.

Cred că poate trebuie să folosesc un tip tuple, astfel încât o comandă să fie definită nu doar ca GetRefs sau altceva, ci ca [GetRefs, Promise<Ref[]>. Nu sunt sigur unde sau cum să fac asta.

Răspuns #1

Poate ai putea face ceva de genul următor:

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)
    }
}

Am adăugat un parametru de tip suplimentar M clasei tale care reprezintă maparea de la intrare la ieșire ca un union de tuple. Când apelezi new CommandBus(context), valoarea lui M va fi dedusă ca valoarea sa implicită never, ceea ce înseamnă că un astfel de command bus nu poate gestiona inițial nicio comandă.

Apoi, când apelezi handle() pe o valoare de tip CommandBus<T, C, M>, returnezi o valoare de tip CommandBus<T, C, M | [H, R]> unde H este tipul comenzii tratate și R este tipul rezultatului. Reține că acest lucru presupune folosirea lanțuirii metodelor pentru a adăuga handleri. De exemplu, dacă apelezi const newBus=bus.handle(x, y), ar trebui să încetezi să mai folosești bus și să începi să folosești newBus, deoarece doar newBus este cunoscut de compilator ca fiind capabil să gestioneze instanțele x.

Și în cele din urmă, atunci când apelezi do() pe o valoare de tip CommandBus<T, C, M>, aceasta permite să transmiți doar un command de un tip găsit în M (M[0] este reuniunea tuturor comenzilor gestionabile). Iar valoarea returnată este tipul rezultat analog (Extract<M, [H, any]> extrage unicul element din reuniunea M care corespunde cu H, iar apoi Extract<M, [H, any]>[1] obține tipul rezultat.

Să vedem dacă funcționează, folosind lucruri inventate:

const b = new CommandBus({ a: "hello" })
    .handle(Date, (t, c) => c.getFullYear() + " " + t.a) 
    .handle(RegExp, (t, c) => c.test(t.a));

Aici fac concatenarea metodelor astfel încât singura instanță CommandBus pentru care am un nume este cea deja configurată să gestioneze valori Date și RegExp. Și iată:

const d = b.do(new Date()).toUpperCase(); // string
console.log(d); // 2020 HELLO
const r = b.do(/hello/); // boolean
console.log(r); // true

Arată bine pentru mine. Bine, sper că asta ajută. Mult noroc!

Link de teren de joacă către cod

Răspunsul #2

Din câte înțeleg, cauți o modalitate de a schimba handle din

handle<HandledCommand extends Command, Result>(
    commandType: Type<HandledCommand>,
    handler: HandlesCommands<Context, HandledCommand, Result>,
  ): CommandBus<Context, Command> {
    this.handlers.set(commandType, handler)
    return this
  }

către

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
  }

Este corect?

Dacă da, răspunsul scurt este „nu”. Ai nevoie de o referință la obiectul clasei pentru commandType pentru a-l folosi la timpul de execuție ca și cheie pentru handleri. Poți obține tipul lui commandType din argumentul handler, dar nu poți obține o referință la obiectul clasei.

Distincția este între spațiu de declarație de tip și spațiu de declarație variabilă -- mai multe despre asta în TypeScript Deep Dive: Declaration Spaces.

Deoarece nu poți obține obiectul clasei dintr-un tip, orice răspuns la întrebarea ta va însemna rearanjarea codului astfel încât obiectul clasei să fie transmis ca parametru într-un alt mod -- dar va trebui totuși să fie transmis la fiecare apel la handle într-un fel.