3

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.

Geza Kerecsenyi
  • 1,127
  • 10
  • 27
  • Have you already looked into https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist ? – ford04 Jan 10 '21 at 15:23
  • @ford04 It appears that the accepted answer there simply provides a union type, which is not the XOR that I am looking for. However, I will look further through those answers, since it seems that there might be some promising stuff there. – Geza Kerecsenyi Jan 10 '21 at 15:24
  • 1
    @GezaKerecsenyi - could you please provide some context of how you are planning to use your constructed type `InvertedKeys` ? How would the consumer code call it and what are the key use-cases? Thank you! – ironstone13 Jan 10 '21 at 17:04
  • @ironstone13 Sure, see now. – Geza Kerecsenyi Jan 10 '21 at 17:24
  • Thanks! If you could clarify one more thing - I do realize that _discriminating unions_ is NOT exactly what you're looking for. However, this is a simple and widely accepted approach - that users of your library would understand well. Sure, one could still create a type that has email, phone and whatnot and pass it into your `handleUserData` method. And TS type checking would allow that, simply because it works like that - it is set based. If I'm not mistaken, function param types are contravariant with `--strictFunctionTypes` – ironstone13 Jan 10 '21 at 17:42
  • 2
    I translated the answer from the other question here to get [this](https://tsplay.dev/4WyGnN). – jcalz Jan 10 '21 at 18:15
  • @jcalz Thanks so much! I tried looking a bunch for existing posts, but clearly missed the one marked as duplicate. No worries, though - at least I have an answer now! – Geza Kerecsenyi Jan 10 '21 at 18:32
  • 1
    Thanks @jcalz - I really learned something new today, thanks to you! Could you please provide a reference, or explain why the `T extends infer U ` condition is needed in `Id` - I would really like to understand this fully. – ironstone13 Jan 10 '21 at 20:23
  • 1
    You could dispose of `Id` entirely; it's just a trick to get the compiler to "expand" out an object type into an explicit list of its properties. See [this](https://stackoverflow.com/questions/57683303/how-can-i-see-the-full-expanded-contract-of-a-typescript-type) which calls the operation `Expand`. – jcalz Jan 10 '21 at 21:34

0 Answers0