5

If I type the following:

interface A {
    x: number
}
interface B {
    y: number
}

type Z = A | B;

// here it allows me to create a variable of type Z with both members of type A and B.
let z: Z = {
    x: 5,
    y: 6,
}

I'm not able to ensure that object of type Z does contain all members of A but does not contain members of B (or the opposite). Is it possible at all with TypeScript? After much research I'm leaning to the "no" answer but I'm not sure.

lort
  • 1,458
  • 12
  • 16
  • Does this answer your question? [Does Typescript support mutually exclusive types?](https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types) – KyleMit Jul 27 '21 at 17:44

1 Answers1

5

By default unions merge all options offered into one, but there are two options for you. What you want exactly is not available in TypeScript, but there are similar issues in the TS project's list (status: "open" at this time) as well as two good workarounds.

One option is not available: At this time there are no exact types in TypeScript (unlike in Flow). There is TS issue #12936 "Exact Types" that right now still is open.

There is another issue/proposal in TS's list of open issues that asks exactly for what your question is about: #14094 "Proposal: Allow exclusive unions using logical or (^) operator between types"

You can use these workarounds:

Option #1

In TypeScript as well as in Flow you can use tagged types to created an XOR union instead of an OR one.

interface A {
    kind: 'A',
    x: number
}
interface B {
    kind: 'B',
    y: number
}

type Z = A | B;

// here it DOES NOT allow to create a variable of type Z with both members of type A and B.
let z: Z = {
    kind: 'A',
    x: 5
    // y: 6 will produce an error
}

Option #2

The second option is to set all properties on all types, but set those that are not supposed to exist to undefined:

interface A {
    x: number,
    y?: undefined
}
interface B {
    x?: undefined,
    y: number
}

type Z = A | B;

let z: Z = {
    y: 5
    // y: 6 will produce an error
}

For the record, in Facebook's type system Flow you can solve the problem by using a disjoint union (XOR) instead of just a union (OR) or by using exact object types or as for TS above setting the undesirable property to undefined. I made a Flow demo with your example (link). In this case the code is the same for Flow as for TS.

Mörre
  • 5,699
  • 6
  • 38
  • 63
  • Perhaps I'm missing something, but I don't think this example demonstrates the problem OP has: this example uses _only members from interface `A`_ in `z` , which is what one would expect with union type (given the typical symbology of `|`). I thought the confusion arises when you mix `A` and `B`. – msanford Nov 15 '17 at 19:25
  • @msanford I'm confused by your confusion, this is what this solves, exactly. Try using BOTH and you get an error. – Mörre Nov 15 '17 at 19:26
  • In the final example, you copied the line "here it allows me to create a variable of type Z with both members of type A and B." yes it does not, it has members _only of A_. Perhaps I would find an example that provokes an error more illustrative. – msanford Nov 15 '17 at 19:28
  • ...as you have just added. :) I still find the inclusion of that line confusing, as implementing disjoint union _prevents_ you from doing exactly that. – msanford Nov 15 '17 at 19:28
  • The first workaround is gold, solves exactly my case! I read about "discriminated unions" in TypeScript documentation because that name sounded good but documentation does not clarify that when using discriminant field, suddenly fields from the other interface are not allowed. It's kinda ugly construction but I'm happy it works. Btw there are some attempts in TS to make filters and/or subtractive types that will probably allow the same but without using discriminant fields. – lort Nov 16 '17 at 12:19