Type interface a or interface b, but not union of both

Viewed 56

Suppose I have the two types:

interface A {
    a: string;
    b: string;
}

interface B {
    a: string;
    c: string;
}

I would like to define type X as A OR B, i.e.

{
    a: string;
    b: string;
}

// OR

{
    a: string;
    c: string;
}

// and NOT:

{
    a: string;
    b: string;
    c: string;
} // (equivalent to X | Y)

// and NOT:

{
    a: string;
} // (equivalent to X & Y)

I have been trying to define some type function which is able to return a type which is either of the two interfaces passed. Here is what I have so far:

type InvertedKeys<A extends object, B extends object> = {
    [P in keyof Omit<B, keyof A>]: never;
} & {
    [P in keyof A]: A[P];
}

// ExclusiveUnion<A, B> should in theory return A OR B
type ExclusiveUnion<A extends object, B extends object> = InvertedKeys<A, B> & InvertedKeys<B, A>;

However, the above is unfortunately too restrictive - it does not allow the base cases

{
    a: string;
    b: string;
}

or

{
    a: string;
    c: string;
}

as it complains that Type 'string' is not assignable to type 'never'.

The reason I am looking to do this is because I would like to have two different options for extending a class when defining a type: for instance:

interface CommonUser {
    username: string;
}

interface UserRegisteredByPhone extends CommonUser {
    phoneNo: string;
}

interface UserRegisteredByEmail extends CommonUser {
    email: string;
}

// What should this function `ExclusiveUnion` be defined as?
type User = ExclusiveUnion<UserRegisteredByPhone, UserRegisteredByEmail>;

function handleUserData(user: User) {
    // The user either has a phone number or an email address, but not both.
    /*
     I am aware that type guards/type checking functions could be used here, but
     I (as well as others using my library) would like to be able to see suggestions
     in the IDE/at build-time that it should be one or the other, to avoid confusion -
     e.g. if I provided both a phone number AND an email address, which one would
     be used? Would it throw a runtime error? Would it silently fail? One should not
     have to check the source code of this function in order to answer these.
    */
}

Is this even possible in Typescript? If so, how could I amend my function to correctly identify one interface or the other, but not both? If possible, I would like to avoid using runtime type guards, in order to maximise ease of use and IDE compatibility.

0 Answers
Related