-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
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. |
#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: '' } // ❌ |
without exact types the new operator does not solve the issue, since |
This would be really helpful, Allows better management in development time. |
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 }; |
@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 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; } |
Search terms so I can find this later: "mutually exclusive" "disjoint unions" |
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. |
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 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 |
I'm all for the But I also think TS needs to recognize and leverage mutually-exclusive union patterns, regardless of the sugar. For example, 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 |
We already turn unit type contradictions ( Going further than that is somewhat dangerous because it means we'd produce Also, sometimes you want to intersect two types in a way where you're using the part of it that isn't contradictory. |
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 Finally, even if you really want to maintain |
It seems like this operator should result in closed types rather than open types like the union operator. |
I think I figured this out. By introducing a 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: |
@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: 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. |
* 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.
What about long chains of 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
Related ts-essentials/ts-essentials#183 |
@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. |
hello, |
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. |
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>; |
so Is there a best practice for implementing type now ? the discussion is too long. we can use |
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 (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. |
I was recently led here indirectly when asking on Stackoverflow about this functionality. It's roughly a year or so since the last comment so I figured I'd voice one. It feels that from an end user perspective the ability to say something is "one of these types" in a native built-in way would be extremely helpful. This stackoverflow post of mine lays out a straight forward use case example: https://stackoverflow.com/q/78656695/5117487. It could possibly have better intellisense than options you will see suggested such as ts-essentials XOR or ExclusifyUnion custom type def from https://stackoverflow.com/a/46370791/5117487. I've read through the comments and as someone on the newer side of learning typescript I was wondering if it can be shared if there is technical limitations that block this from being native to TS? Or even design philosophical reasons why it should not be included? Not so as to put pressure on why it does not exist today but I think it is what I see missing from this thread thus far in what mostly became driven by external library solution talks. |
This would not prevent from the additional properties to exist, because additional properties are allowed to exist. It would only work for excess property checks and object literals, which is not really what the people want. An operator like this would only really make sense when we have Exact Types (#12936). |
Thanks for the reply @MartinJohns
I don't follow this part - the linked "ExclusifyUnion" suggestion (very popularly pointed to online) and ts-essentials XOR does stop the properties from existing. You can see this in this playground. Maybe you are referring to one of the few suggested approaches in this issue specifically? I personally am not suggesting one specific one (I am at this point too ignorant of under the hood of TS to be making specific implementation suggestions yet). Though I could be misunderstanding depending on how you are distinguishing between blocking "additional properties ... exist[ing]" and "excess property checks". My current level of understanding interprets that as the same thing? From all the stackoverflow posts / blog posts I've read on this it seems to be exactly what many people want (blocking extra properties) so again not sure what you are referencing when you say it is not what the people want. I read through the Exact Types suggestion you linked and yes I do think (from what I can tell) that it is one way to solve this. Though you say "when we have" but I don't get any stronger sense from reading all the comments there that it is committed to versus something like proposed in any of the comments here (except for of course that this one was closed). |
here in 2024 and I would be happy for it. |
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
For literal and primitive types it should behave like union type:
The text was updated successfully, but these errors were encountered: