I have these two interfaces:

export interface ResLogin {
    e: "already logged in" | "fail";
    ticket: string;
}

export interface ResException {
    e: "exception";
    msg: string;
    stack: string;
}

and I want a type that have all members of ResLogin and ResException, while 'e' field have possible values of "already logged in", "fail" and "exception".

This is my attempt:

export type ResLoginClient = ResLogin & ResException;
export type ResLoginClient2 = ResLogin | ResException;

However both have problems:

let r: ResLoginClient;
r.e // r.e has 'never' type
r.msg // r.msg exists and that's good

let r2: ResLoginClient2;
r2.e // type of r2.e is "already logged in" | "fail" | "exception"
r2.msg // but r2.msg does not exists

I want some way of declaring a type so that

let r3:????
r3.e // type of r3.e is "already logged in" | "fail" | "exception"
r3.msg // exists
r3.ticket // exists

How can I declare such type?

1 Answers

0
jcalz On Best Solutions

I don't understand your use case exactly so it's possible this type function isn't exactly what you need:

type Combine<U> = {
  [K in U extends any ? keyof U : never]: U extends { [P in K]?: any }
    ? U[K]
    : undefined
};

type ResLoginClient = Combine<ResLogin | ResException>;
// type ResLoginClient = {
//    e: "already logged in" | "fail" | "exception";
//    ticket: string | undefined;
//    msg: string | undefined;
//    stack: string | undefined;
// }

or, if you don't want those undefined properties, you can do

type Combine<U> = {
  [K in U extends any ? keyof U : never]: U extends { [P in K]?: any }
    ? U[K]
    : never // undefined became never here
};

type ResLoginClient = Combine<ResLogin | ResException>;
// type ResLoginClient = {
//    e: "already logged in" | "fail" | "exception";
//    ticket: string;
//    msg: string;
//    stack: string;
// }

Hope one of those is useful to you; good luck!


UPDATE: now that I know that these types are useful to you, I can explain anything that's confusing about how they work. There are mapped and conditional types in there, so you might want to read up on those for more details.

So Combine<U> is a mapped type, whose keys are U extends any ? keyof U : never. That's a distributed conditional type which is not just the same as keyof U (which would just be "e" as you noted). Since the U in U extends any is a bare type parameter, if U is a union type, the compiler will split the union up into its constituents, do the conditional type for each one, and then make a union of the results. So when U is ResLogin | ResException, the conditional type U extends any ? keyof U : never is evaluated as

(ResLogin extends any ? keyof ResLogin : never) | 
(ResException extends any ? keyof ResException : never)

or

(keyof ResLogin) | (keyof ResException)

So what I've done is distribute the keyof function over the union in U. And thus K of the mapped type will iterate over the keys that appear in at least one constituent of U.

The properties in the mapped type are similarly distributed. We have U extends { [P in K]? any } ? U[K] : undefined. This becomes, for our U, (ResLogin extends {[P in K]?: any} ? ResLogin[K] : undefined) | (ResException extends {[P in K]?: any} ? ResException[K] : undefined). This is evaluated for each K in the mapped type, so let's just look at one for concreteness, say "ticket". The mapped type {[P in K]?: any} where K is just "ticket" is evaluated to the concrete type {ticket?: any}. So the "ticket" property of Combine<U> will be evaluated as:

(ResLogin extends {ticket?: any} ? ResLogin['ticket'] : undefined) |
(ResException extends {ticket?: any} ? ResException['ticket'] : undefined) 

Now ResLogin does extend {ticket?: any}, and ResException does not extend {ticket?: any}, so that property is evaluated as:

(ResLogin['ticket']) | (undefined)

So ResLogin contributes its "ticket" property, and ResException contibutes undefined. If, instead of "ticket", we look at "e", which appears in both union constituents, the property will become:

(ResLogin['e']) | (ResException['e'])

and there won't be an undefined in there.

Hope that makes sense now. Good luck again!