Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Add an "exclusive or" (^) operator #14094

Closed
mohsen1 opened this issue Feb 15, 2017 · 43 comments
Closed

Proposal: Add an "exclusive or" (^) operator #14094

mohsen1 opened this issue Feb 15, 2017 · 43 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@mohsen1
Copy link
Contributor

mohsen1 commented Feb 15, 2017

Based on this comment TypeScript does not allow exclusive union types.

I'm proposing a logical or operator similar to union (|) or intersection (&) operators that allows defining types that are one or another.

Code

type Person = { name: string; } ^ { firstname: string; lastname: string; };

const p1: Person = { name: "Foo" };
const p2: Person = { firstname: "Foo", lastname: "Bar" } ;

const bad1: Person = { name: "Foo", lastname: "Bar" }
                                    ~~~~~~~~~~~~~~~   Type Person can not have name and firstname together 
const bad2: Person = { lastname: "Bar", name: "Foo" }                                            
                                        ~~~~~~~~~~~   Type Person can not have lastname and name together

For literal and primitive types it should behave like union type:

// These are the same 
type stringOrNumber = string | number;
type stringORNumber = string ^ number;
@mhegazy
Copy link
Contributor

mhegazy commented Feb 15, 2017

The issue is there are no exact/final types in TS. all types are open ended. so this is allowed from an assignable perspective:

var p1: {name: string };
var p2 =  {name:"n", firstName: "f", lastName: "l"};
p1 = p2;  // OK

A type is assignable to a union type, iff it is assignable to one of the constituents. so even with the exclusive union, open types would allow that check to pass.

Another feature that TS has is flagging "unknown" properties. This only applies to object literals, that happen to have a contextual type. e.g.:

var p: {name: string} = { name: "n", another: "f" }; // Error `another` is not a known property

This check is simplified for union types, just to say it has to be a "known" property on the union in general, not for this constituent. The main issue here is how unions are compared, when we are comparing constituent types, we do not know if we should check for "unknown" properties, because it might be one of the other types.

If i am not mistaken, you are after this unknown property check in this case. I would say we are better off trying to change how this is handled for unions, rather than creating a new concept in the language that users need to understand.

Related discussion in #12997. Issue tracked by #12745

@mohsen1
Copy link
Contributor Author

mohsen1 commented Feb 16, 2017

#12745 will solve parts of this problem. But it won't have exclusive or logic because tagged unions are merging assignable types. For example:

interface A { foo: string; }
interface B { bar: string; }

type C = A | B;

const a: C = { foo: '' } // Wrongfully ✔ // Hopefully #12745 fixes this
const b: C = { bar: '' } // Wrongfully ✔ // Hopefully #12745 fixes this
const c: C = { foo: '', bar: '' } // ✔

What I'm suggesting is an exclusive or operator between two interfaces:

interface A { foo: string; }
interface B { bar: string; }

type C = A ^ B;

const a: C = { foo: '' } // ✔
const b: C = { bar: '' } // ✔
const c: C = { foo: '', bar: '' } // ❌

@mhegazy
Copy link
Contributor

mhegazy commented Feb 16, 2017

without exact types the new operator does not solve the issue, since { foo: '', bar: '' } is still a { foo: string; }

@mukhuve
Copy link

mukhuve commented Mar 10, 2017

This would be really helpful, Allows better management in development time.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Mar 10, 2017
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 10, 2017

Presumably the behavior here would be that

type A = { m: T } ^ { n : U };

is equivalent to

type A = { m: T, n: undefined } | { m: undefined, n : U };

@mohsen1
Copy link
Contributor Author

mohsen1 commented Mar 10, 2017

@RyanCavanaugh it makes it even easier to implement as de-sugar algorithm is so easy!

This operator is still a good idea since there are lots of cases where an API expect two or more completely different interfaces.

Adding undefined can pile up quickly:

type A = { m: T } ^ { n:  U } ^ { o: Q }

vs.

type A = 
    { m: T; n: undefined; o: undefined; } |
    { m: undefined; n: U; o: undefined; } |
    { m: undefined; n: undefined; o: Q; }

@RyanCavanaugh
Copy link
Member

Search terms so I can find this later: "mutually exclusive" "disjoint unions"

@battmanz
Copy link

battmanz commented Jun 2, 2017

I'd like to add another use case for this feature. I'm currently using json-schema-to-typescript to generate TypeScript interfaces from JSON Schema. Currently the "oneOf" keyword is listed as not expressible in TypeScript. Having exclusive unions would then make it expressible.

@lukescott
Copy link

lukescott commented Nov 15, 2017

With this example:

type A = { m: T } ^ { n:  U } ^ { o: Q }
// ->
type A = 
    { m: T; n: undefined; o: undefined; } |
    { m: undefined; n: U; o: undefined; } |
    { m: undefined; n: undefined; o: Q; }

This actually doesn't work:

// Assume T, U, and Q are string:
let a = { // <-- error
  m: ""
}

Because it expects:

// Assume T, U, and Q are string:
let a = {
  m: "",
  n: undefined, // <-- expects undefined value
  o: undefined,  // <-- expects undefined value
}

You have to define it like this:

type A = 
    { m: T; n?: undefined; o?: undefined; } |
    { m?: undefined; n: U; o?: undefined; } |
    { m?: undefined; n?: undefined; o: Q; }

But that allows you to put n: undefined on the object, which makes the key enumerable. So far I've found this works out better:

type A = 
    { m: T; n?: never; o?: never; } |
    { m?: never; n: U; o?: never; } |
    { m?: never; n?: never; o: Q; }

I wish there was an ^ operator. I've been bitten by thinking | was supposed to be an exclusive OR. Thinking about bitwise it makes sense. But I almost always want ^ instead of |.

@krryan
Copy link

krryan commented Mar 15, 2018

I'm all for the ^ sugar, that's great.

But I also think TS needs to recognize and leverage mutually-exclusive union patterns, regardless of the sugar. For example, { kind: 'foo'; } | { kind: 'bar'; } is mutually exclusive because kind cannot possibly be 'foo' and 'bar' simultaneously. Likewise with literal number types. And naturally, { m: T; n?: never; } | { m?: never; n: U; } under discussion as a de-sugared { m: T; } ^ { n: U; } is mutually exclusive as well. None of these types is mutually exclusive because of any specific syntax, they just are exclusive by their very nature.

But TS doesn't recognize or leverage that fact as strongly as it could. For examples, #20375 and #21879 could benefit from recognizing mutually exclusive unions to allow for narrowing (that would not be safe with mutually inclusive unions). So I think this request should be more than just sugar. I would expect something like declare const test: { kind: 'foo'; } & { kind: 'bar'; } to result in test: never, but it doesn't, and TS will happily allow you to pass test to anything that's expecting a { kind: 'foo'; } or anything that's expecting a { kind: 'bar'; }.

@RyanCavanaugh
Copy link
Member

We already turn unit type contradictions ("foo" & "bar") into never during union/intersection type distribution.

Going further than that is somewhat dangerous because it means we'd produce never in a way that was fundamentally un-debuggable - imagine intersection two types and deep inside two uninteresting properties conflict and the whole thing collapses to never and you'd have no way to figure out why.

Also, sometimes you want to intersect two types in a way where you're using the part of it that isn't contradictory.

@krryan
Copy link

krryan commented Mar 15, 2018

I agree with the debugging issue; having a way to investigate type inference would be really useful in general but I imagine that would be very difficult to offer.

But I still think never is the correct type for this case. Omit could be used, perhaps, to specify that you weren't interested in that bit that conflicts; certainly on our project, that would be an expectation that we'd have of our coders, to be explicit about something like that.

Finally, even if you really want to maintain { kind: 'foo'; } & { kind: 'bar'; }, test.kind should still have the type never. It currently has the type 'foo' & 'bar', which is, again, impossible.

@Griffork
Copy link

It seems like this operator should result in closed types rather than open types like the union operator.

@mohsen1 mohsen1 changed the title Proposal: Allow exclusive unions using logical or (^) operator between types Proposal: Add an "exclusive or" (^) operator Mar 16, 2018
@mohsen1
Copy link
Contributor Author

mohsen1 commented Mar 16, 2018

I think I figured this out. By introducing a Without generic that forces all properties in an object to not present we can construct a working XOR generic:

FYI @isiahmeadows

type Without<T> = { [P in keyof T]?: undefined };
type XOR<T, U> = (Without<T> & U) | (Without<U> & T)

type NameOnly = { name: string };
type FirstAndLastName = { firstname: string; lastname: string };
type Person = XOR<NameOnly, FirstAndLastName>

const p1: Person = { name: "Foo" };
const p2: Person = { firstname: "Foo", lastname: "Bar" } ;

const bad1: Person = { name: "Foo", lastname: "Bar" }
const bad2: Person = { lastname: "Bar", name: "Foo" }                                            

Works in 2.7:

screen shot 2018-03-16 at 12 42 02 am

@SylvainEstevez
Copy link

SylvainEstevez commented Mar 16, 2018

@mohsen1 Hat off! 🎩

I've tried to play with your solution a bit, and so far I haven't been able to find a solution that allows common property names to remain in the resulting type. Below a simple example:
screen shot 2018-03-16 at 12 15 02 pm

In the real life use cases I have for an exclusive OR type, objects generally have at least some properties in common.

I guess that joins the mutually exclusive concerns in the previous comments.

SirensOfTitan added a commit to KintabaHQ/phelia that referenced this issue Oct 8, 2020
* Upgraded packages to latest versions.
  This included a need to pin `@types/react` for the
  `@types/react-reconciler` package, as the latter seems stale.
* Removed unneeded dependencies: `ts-xor` is two lines of code
recommended by a person in a typescript issue (see:
microsoft/TypeScript#14094)
* Removed all examples: I'm considering this commit a real fork of
phelia, and I don't want to maintain examples right now.
* Removed non-production dependencies: react-dom isn't needed, express
isn't needed either.
* Upgrade all packages to least versions that remain.
@sobolevn
Copy link

sobolevn commented Nov 8, 2020

What about long chains of XOR types? People might have use-cases (like I do) when you have to XOR more than 40 types.

This is how it looks:

export type Task =
    XOR<{ a: AModule },
      XOR<{b: BModule }, 
        XOR<{ c: CModule }, 
          XOR<{ d: DModule }, 
            XOR<{ e: EModule }, { f: FModule }> // ...
          >
        >,
      >
    >;

I would perfer to have:

XOR<
  { a: AModule },
  { b: BModule },
  { c: CModule },
  { d: DModule },
  { e: EModule },
  { f: FModule },
  ...
>

or


^ { a: AModule },
^ { b: BModule },
^ { c: CModule },
^ { d: DModule },
^ { e: EModule },
^ { f: FModule },
//  ...

Related ts-essentials/ts-essentials#183

@krryan
Copy link

krryan commented Nov 8, 2020

@sobolevn The former isn’t too hard to write in Typescript 4.1:

type AllXOR<T extends any[]> =
    T extends [infer Only] ? Only :
    T extends [infer A, infer B, ...infer Rest] ? AllXOR<[XOR<A, B>, ...Rest]> :
    never;
AllXOR<[
  { a: AModule },
  { b: BModule },
  { c: CModule },
  { d: DModule },
  { e: EModule },
  { f: FModule },
  ...
]>

Before 4.1, you don’t have conditional recursive types, which makes this a pain. There are often ways to get around that, but I don't know what you would need to do to satisfy the compiler there off the top of my head.

Even in 4.1, you will probably run into the “recursion is too long and possibly infinite” error pretty quickly—probably before you could XOR 40 types.

@mishoo78
Copy link

hello,
so this is a 6 years old topic and nothing was done so far; looking at the roadmap, nothing is going to be done.
my question is at this point in time, what is the reason for NOT even discussing this?
what are the technical challenges that prevents this feature to be implemented?
is there an elegant alternative (i do find this xor proposal an ugly hack)
is there an official position / statement / blog ...anything that would at least close this matter?

@RyanCavanaugh
Copy link
Member

Every suggestion here is approximately 6 to 8 years old, that's what happens when your programming language is ten+ years old and has been popular for 6 to 8 years. If your bar for "it should be done by now" is "it's six years old", then you think TypeScript should have approximately every feature anyone's ever thought of.

@kryshac
Copy link

kryshac commented Jul 3, 2023

I rewrote the XOR type a bit, which can accept more parameters. The problem for me was not this, it was the problem with "recursion is too long and possibly infinite" @krryan said that the limit was 40 but for me the error appeared from the 10th type, possibly because my types were more complex, I didn't have tested for my variant which is the limit but with 11 it works ok

export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };

export type SkipUnknown<T, U> = unknown extends T ? never : U;

export type XOR<
  A,
  B,
  C = unknown,
  D = unknown,
  E = unknown,
  F = unknown,
  G = unknown,
  H = unknown,
  I = unknown,
  J = unknown,
  K = unknown,
> =
  | (Without<B & C & D & E & F & G & H & I & J & K, A> & A)
  | (Without<A & C & D & E & F & G & H & I & J & K, B> & B)
  | SkipUnknown<C, Without<A & B & D & E & F & G & H & I & J & K, C> & C>
  | SkipUnknown<D, Without<A & B & C & E & F & G & H & I & J & K, D> & D>
  | SkipUnknown<E, Without<A & B & C & D & F & G & H & I & J & K, E> & E>
  | SkipUnknown<F, Without<A & B & C & D & E & G & H & I & J & K, F> & F>
  | SkipUnknown<G, Without<A & B & C & D & E & F & H & I & J & K, G> & G>
  | SkipUnknown<H, Without<A & B & C & D & E & F & G & I & J & K, H> & H>
  | SkipUnknown<I, Without<A & B & C & D & E & F & G & H & J & K, I> & I>
  | SkipUnknown<J, Without<A & B & C & D & E & F & G & H & I & K, J> & J>
  | SkipUnknown<K, Without<A & B & C & D & E & F & G & H & I & J, K> & K>;

@yangliguo7
Copy link

so Is there a best practice for implementing type now ?

the discussion is too long.

we can use ^ now ? @RyanCavanaugh or use XOR like @mohsen1 said ?

@maninak
Copy link

maninak commented Aug 28, 2023

If you ended up here from Google, I've packaged up a working solution complete with tests and documentation and published it on npm as ts-xor.

(shameless plug blah blah but I've ended up needing this way too often and thought I'd make a neat shareable solution I wouldn't feel bad introducing as a dependency at work)

EDIT: ts-xor now supports XORing together up to 200 types, which will hopefully meet the needs that some community members couldn't satisfy with the solutions shared in this issue until now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests