Typescript, create a Mediator resolver for classes

Viewed 51

I'm new with Typescript and I'm trying to create something like MediatR for C# in Typescript.

I've this scenario:

Interfaces:

export interface ICommand {}

export interface ICommandResult {}

export interface ICommandHandler<ICommand, ICommandResult> {
    execute(request: ICommand): ICommandResult;
}

implementations:

export interface ConcreteCommand extends ICommand {
    name: string;
}

export interface ConcreteResult extends ICommandResult {
    name: string;
}

export class ConcreteCommandHandler
    implements ICommandHandler<ConcreteCommand, ConcreteResult> {
    public execute(request: ConcreteCommand): ConcreteResult {
        let result: ConcreteResult = {
            name: request.name
        }

        return result;
    }
}

What I want to reach is something like this:

//pseudocode!
let command: ConcreteCommand = { 
 name: "mycommand!";
}

let handler = CommandHandlersFactory().Create<ConcreteCommand>(command);
let result = handler.execute();

But I'm stuck inside the "CommandHandlersFactory().Create()" implementation because I don't know to to resolve the right CommandHandler with the right key (type).

My idea was to have a Map<ICommand, ICommandHandler>() so I can do something like this:

//Psudocode!
_map<ICommand, ICommandHandler>() = new Map();
_map.set(ConcreteCommand, ConcreteCommandHandler);
 
Create<T>(command: ICommand) {
 let concreteCommandHandlerType = _map[command];
 return new concreteCommandHandlerType();
}

Is even possibile to do something like this in Typescript?

Thanks a lot!

1 Answers

Using interfaces

The thing is that interfaces does not exist in final transpilled JS code, so you cannot reference them. If you want to work with interfaces, not classes, it is not possible to do that in "vanilla" TypeScript like you want, but you can use eg. tst-reflect, which is TypeScript transformer generating metadata for runtime reflection. It works with type parameters, so it is exactly what you want.

Here is working demo on StackBlitz.

import { CommandHandlerFactory } from './src/CommandHandlerFactory';
import { HandlerCollection } from './src/HandlerCollection';
import { ICommand } from './src/ICommand';
import { ICommandHandler } from './src/ICommandHandler';
import { ICommandResult } from './src/ICommandResult';
import { Mediator } from './src/Mediator';

export interface ConcreteCommand extends ICommand {
  name: string;
}

export interface ConcreteResult extends ICommandResult {
  name: string;
  someOthrPropsToSeeThatItIsRlyResult: string;
}

export class ConcreteCommandHandler
  implements ICommandHandler<ConcreteCommand, ConcreteResult>
{
  public execute(request: ConcreteCommand): ConcreteResult {
    let result: ConcreteResult = {
      name: request.name,
      someOthrPropsToSeeThatItIsRlyResult: 'Okay, it is!',
    };

    return result;
  }
}

const collection = new HandlerCollection();
collection.addTransient<ConcreteCommand>(ConcreteCommandHandler);

const handlerFactory = new CommandHandlerFactory(collection);
const mediator = new Mediator(handlerFactory);

let command: ConcreteCommand = {
  name: 'mycommand!',
};

const result = mediator.send(command);
console.log(result);

Output:

{
  name: 'mycommand!',
  someOthrPropsToSeeThatItIsRlyResult: 'Okay, it is!'
}

All the imported files are in that StackBlitz. I didn't copy that here, it is quite a lot of code.

Just to see some reflection, here is the HandlerCollection:

import { getType, Type } from 'tst-reflect';
import { ICommand } from './ICommand';

export type HandlerCtor = { new (...args: any[]) };

export class HandlerCollection {
  private readonly handlers = new Map<string, HandlerCtor>();

  addTransient<TCommand extends ICommand>(handler: HandlerCtor) {
    this.handlers.set(getType<TCommand>().fullName, handler);
  }

  getHandler<TCommand extends ICommand>(
    commandType?: Type
  ): HandlerCtor | undefined {
    commandType ??= getType<TCommand>();
    return this.handlers.get(commandType.fullName);
  }
}

Using classes

In case you are okay with using classes, I found existing port of MediatR on NPM. Or you can use my example and just replace Type by class' constructor.

Related