How to store "types" and instantiate them dynamically in typescript?

66 Views Asked by At

I'm trying to accomplish a similar behavior that I can do with C#. In C# I can use reflection to dynamically instantiate a class based on the Type class. The following code shows how to do in c#, rather simple:

interface IHandler
{
   void Handle();
}
var handlers = new Dictionary<string, Type>
{
  { "querydom", typeof(QueryDomHandler) },
  { "other", typeof(OtherHandler) },
};

var type = handlers["querydom"];
var instance = (IHandler)Activator.CreateInstance(type, args);
instance.Handle();

How can I accomplish the same with typescript? I've sketched the following code, but I do not know how to get a "Type" from a class (QueryDomCommandHandler), or how to instantiate a class dynamically without using its name ( new QueryDomCommandHandler()).

let handlers = [];
handlers[CommandType.QueryDom] = QueryDomCommandHandler; //how to store the "type"?

chrome.runtime.onMessage.addListener((message: Command, sender, sendResponse) => {
    logger.debug(`${isFrame ? 'Frame' : 'Window'} '${document.location.href}' received message of type '${CommandType[message.command]}'`);
 
    const handlerType = handlers[CommandType.QueryDom];
    const handlerInstance = ????? //how to instantiate the class?
    if (message.command == CommandType.QueryDom) {
        const handler = new QueryDomCommandHandler(message as RulesCommand);
        const response = handler.handle();
        sendResponse(response);
        return true;
    }
    else if (message.command == CommandType.Highlight) {
        const handler = new HighlightCommandHandler(message as RulesCommand);
        handler.handle();
    }
});

Any insights?

UPDATE

Thanks to the answers, here's my solution, although I would like to use the enum instead of the hardcoded string in the Record, but couldn't figure it out:

const handlers: Record<string, (new () => commands.CommandHandlerBase)> = {
    'QueryDom': QueryDomCommandHandler,
    'Highlight': HighlightCommandHandler,
    'ClearHighlight': ClearHighlightCommandHandler,
};

    const handlerType = handlers[commands.CommandType[message.command]];
    const handler = new handlerType();
    const response = await handler.handle(message);
2

There are 2 best solutions below

0
JobaDiniz On BEST ANSWER

It was so simple. You just need to use the Map class instead of the Record:

type WorkerHandlerConstructor = (new () => handlers.CommandHandlerBase);

handlers: Map<types.CommandType, WorkerHandlerConstructor> = new Map<types.CommandType, WorkerHandlerConstructor>([
    [types.CommandType.Click, handlers.ClickCommandHandler],
    [types.CommandType.Navigate, handlers.NavigateCommandHandler]
]);

And then

const handlerType = handlers.get(types.CommandType.Click);
const handler = new handlerType();
3
Jerryh001 On

Note: you can not get "type" in typescript because the type not exist in the runtime, what you get is the constructor of the class actually.

you will need some type on the handlers array first to make the inference works:

let handlers: (new (...arg: any[]) => CommandHandler)[] = [];

modify the CommandHandler part to the name of the base handler class

to create a instance after get the constructor, just new it, the class name itself is not important:

const handlerInstance = new handlerType()
//    ^? const handlerInstance: CommandHandler

Solution for the Update part:

Use satisfices keyword on the handlers is what you want:

const handlers = {
    [CommandType.QueryDom]: QueryDomCommandHandler,
    [CommandType.Highlight]: HighlightCommandHandler,
    [CommandType.ClearHighlight]: ClearHighlightCommandHandler
} satisfies Record<CommandType, (new () => commands.CommandHandlerBase)>

if (message.command == CommandType.QueryDom) {
    const handlerType = handlers[message.command];
    const handler = new handlerType();
    //    ^? const handler: QueryDomCommandHandler
}

And enum key can be used as an object key like above.

Note: satisfies Record<string, (new () => commands.CommandHandlerBase)> has similar effect, but I think use CommandType would be more clear.