Construct type based on given type

Viewed 39

I am trying to implement a request/response type of function. I'd like for the response type to be calculated based on the request type, but I don't want to use conditional types, because that'd mean limiting myself only to known types.

Let's assume we have a request class like this:

abstract class Request { ... }
abstract class Response { ... }

class Example1Request extends Request { ... }
class Example1Response extends Response { ... }
class Example2Request extends Request { ... }
class Example2Response extends Response { ... }

I'd like to be able to declare a query<T extends Request>() function that returns a matching response type. In this case, if the request is Example1Request I'd like the return type to be Example1Response. Essentially it would be a text manipulation to replace the Request suffix with Response.

Is anything like that even possible with TypeScript?

1 Answers

I would use generics like

class MyCustomError extends Error {}

abstract class Message {}
class Example1 extends Message {}
class Example2 extends Message {}

abstract class MessageFactory<T> {
    abstract fromRawData(data: any): T;
}

class Example1Factory implements MessageFactory<Example1> {
    fromRawData(data: any): Example1 {
        if (! data?.property1) {
            throw new MyCustomError();
        } else {
            return new Example1(); // You should build it based on data
        }
    }
}
class Example2Factory implements MessageFactory<Example2> {
    fromRawData(data: any): Example2 {
        if (! data?.property2) {
            throw new MyCustomError();
        } else {
            return new Example2(); // You should build it based on data
        }
    }
}

abstract class MyRequest<T> {
    private factory: MessageFactory<T>;

    constructor(f: MessageFactory<T>) {
        this.factory = f;
    }

    getRequest(data: any): T {
        try {
            return this.factory.fromRawData(data);
        } catch (err) {
            if (err instanceof MyCustomError) {
                console.error("Message has not been parsed");
            }
        }
    }
}

class Example1Request extends MyRequest<Example1> {}
class Example2Request extends MyRequest<Example2> {}

// Apply same logic for responses

const possible_requests: MyRequest<Message>[] = [new Example1Request(new Example1Factory()), new Example2Request(new Example2Factory)]

const data = {
    property2: "yes"
}

possible_requests.forEach((req) => {
    console.log(req.getRequest(data));
})

// Logs in error 
// Message has not been parsed

// Logs in log
// undefined
// Example2 {}

So you will need a Factory to verify eligibility of your Message to your custom type (Example1 for instance).
Then you might build a Request which will use this factory and try to parse your message. It also handles errors in case message is not valid.
Finally you list each requests in an array and ask it to parse your message, you will then only have to filter undefined ones :)

Your Response should be built the same way, listing them and applying the requests to get appropriate response !

Please let me know if somehting is unclear, or if you don't understand how to use reponses based on Message's type

EDIT 1 @hhearts

abstract class MyRequest<T> {
    private factory: MessageFactory<T>;

    constructor(f: MessageFactory<T>) {
        this.factory = f;
    }

    getRequest(data: any): MyRequest<T> {
        try {
            this.factory.fromRawData(data); // Checks factory might build request's message
            return this; // We return request instead of message
        } catch (err) {
            if (err instanceof MyCustomError) {
                //console.error("Message has not been parsed");
            }
        }
    }
}

const message_types: string[] = possible_requests.map((req) => req.getRequest(data)).filter(r => !!r).map(s => s.constructor.name);
console.log(message_types);
// ["Example2Request"]

A Response<Example2> is easy to setup once you determine the Request's type (or Message's one)

For instance working with a singleton of Example2ResponseFactory in Example2Request enabling to build Example2Response from Example2Request based on Example2 (message) :)

EDIT 2

As types are not kept after compilation there is no way to get the "type" however you can access the typename through

variable.constructor.name
Class.name

And compare them, asthey are string you might compare it with an easy

typename.replace("Response/Request", ""); // Use it to compare request and response :)
Related