1
\$\begingroup\$

I am working on a typescript react/redux app that provides a visual interface for the code editor with a predefined set of instructions. It's basically a tree structure of objects that I call Handlers and these can be nested. All these handlers implement a simple IHandler interface and common functionality is implemented in HandlerBase class.

export default interface IHandler {
    id: Id;
    type: string;
    isDisabled?: boolean;
    isExpanded?: boolean;
    codeTemplate: CodeTemplate;
    toJson: () => string;
    toCode: () => string;
}

export default class HandlerBase<Type extends string, T extends HandlerBase<Type, T>> implements IHandler {
    id: Id<T>;
    type: Type;
    isDisabled?: boolean;
    isExpanded?: boolean;
    codeTemplate: CodeTemplate<T>;
}

The Id type is a strongly typed string that makes it easier to save the types of handlers under the ids.

export type Id<T extends IHandler = IHandler> = string & { __type: T["type"] };

All the handlers are saved in a redux store by their keys and refer to other handlers that are nested under them also by the Id type. Also, each handler has it's own unique Type which is a string literal type.

class ExampleHandlerModel extends HandlerBase<"Example", ExampleHandler> {
    child: Id<ExampleHandlerModel>;
}

What I would like to achieve is to strongly type the IHandler.type attribute, which right now is just a string type, automatically as a union of all implemented handlers in my codebase. I looked into class decorators, but since I've never worked with them, I wasn't able to come up with any solution, nor am I sure that this problem can be solved by decorating the deriving classes (my inspiration was C# attributes that can do similar kind of type registration).

Another problem I would like to also solve is that these implementations of handler class at some point in my code need to be instantiated and they also have static methods that should be able to be called based on the type property. The really ugly and unmaintainable way I'm doing it now is a function that takes an IHandler argument and then just runs it through a switch statement that I manually maintain and update every time I implement a new handler type.

export const HandlerGetStatic = <T extends IHandler>(handler: T, attr: string): any => {
    switch (handler.type) {
        case "Example": return (ExampleHandlerModel)[attr];
        case "Foo": return (FooHandlerModel)[attr];
        // etc.
    }
}
export const InstantiateHandler = <T extends HandlerBase<string, T>>(model: Model<T>): IHandler => {
    switch (model.type) {
        case "Example": return new ExampleHandlerModel(model);
        case "Foo": return new FooHandlerModel(model);
        // etc.
    }
}

Here I would also like to remove the need for manual updating of these helpers.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.