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 };
}