Home > database >  Conditional type based on a mapped type
Conditional type based on a mapped type

Time:10-19

I'm working on a type safe variant of a client/server communication, where I used mapped types to define parameters required to send a request and the return values in a response for that request. Here's an example of each type:

export interface IProtocolParameters {
    "authenticate": { username: string; password: string };
    "getLogLevel": {};
}

and

export interface IProtocolFinalResult {
    "authenticate": { activeProfile: ICommShellProfile };
    "getLogLevel": { result: string };

Unfortunately there are APIs which return multiple responses, so I have to collect them in an array. That's why I defined my actual response type and the promise returning it as:

export type ResponseType<K extends keyof IProtocolFinalResult> =
    Array<IProtocolFinalResult[K]> | IProtocolFinalResult[K];

export type ResponsePromise<K extends keyof IProtocolFinalResult> = Promise<ResponseType<K>>;

This has the drawback that everywhere I consume such a result I have first to test if it is an array:

    public async getLogLevel(): Promise<string> {
        const response = await MessageScheduler.get.sendSimpleRequest({
            requestType: "getLogLevel",
            parameters: {},
        });

        return Array.isArray(response) ? response[0].result : response.result;
    }

which is inconvenient (especially given that I have hundreds of APIs). I'm now looking for a way to improve that by using a conditional type that either returns an array of IProtocolFinalResult[K] or just IProtocolFinalResult[K], depending on the API.

From the documentation I understand that you can use a conditional type only based on inheritance, not based on another condition (a flag or a mapped type etc.). Any other idea what I could do here?

What I have in mind is something like:

export type ResponseType<K extends keyof IProtocolFinalResult> = multiResults.has(K) ? Array<IProtocolFinalResult[K]> : IProtocolFinalResult[K];

with

const multiResults: Set<string> = new Set(["listSomething"]);

Here's a playground example for this code..

CodePudding user response:

I understand that you can use a conditional type only based on inheritance, not based on another condition (a flag or a mapped type etc.)

Typescript's string typing allows you to have both. If you have a type Foo = "a" | "b" | "c", then inheritance is equivalent to a "contains" kind of check (because the type "a" is a subtype of "a" | "b" | "c").

type multiResults = "authenticate" | "getLogLevels";

export type ResponseType<K extends keyof IProtocolFinalResult> =
  K extends multiResults
    ? Array<IProtocolFinalResult[K]>
    : IProtocolFinalResult[K];

If you also want the information at runtime, not just at compile time, and still have a single source of truth, that's also possible:

const multiResults = ["authenticate", "getLogLevels"] as const;

export type ResponseType<K extends keyof IProtocolFinalResult> =
  K extends typeof multiResults[number]
    ? Array<IProtocolFinalResult[K]>
    : IProtocolFinalResult[K];

CodePudding user response:

My suggestion is define which response is Array in the IProtocolFinalResult.

For example, authenticate returns array:

export interface IProtocolFinalResult {
    "authenticate": Array<{ activeProfile: ICommShellProfile }>;
    "getLogLevel": { result: string };
}

and then, ResponseType will be:

export type ResponseType<K extends keyof IProtocolFinalResult> = IProtocolFinalResult[K];

after this, when you try to access the response of getLogLevel, it will not be consider as a array anymore.

an extra case:

if authenticate may returns an array, the IProtocolFinalResult can be:

type MaybeArray<T> = T | Array<T>

export interface IProtocolFinalResult {
    "authenticate": MaybeArray<{ activeProfile: ICommShellProfile }>;
    "getLogLevel": { result: string };
}
  • Related