How to define a true logical OR of object types (no mixing of different object keys in result) [duplicate]
Asked Answered
C

1

4

An example:

type TA = {a:number,b:number}
type TB = {a:number,c:number,d:number}
const t1:Or<TA,TB> = {a:1,b:1} // want
const t2:Or<TA,TB> = {a:1,c:1,d:1} // want 
const t3:Or<TA,TB> = {a:1,b:1,c:1} // DON'T want 

The desired result is for t1 to be valid because it exactly fits TA, and t2 to be valid because it exactly fits TB, but for t3 to be invalid because it doesn't exactly fit either TA or TB.

When

type Or<T,U>=T|U

TypeScript actually considers t3 to be valid. TypeScript union type | is also allowing keys objects to be merged and sometimes partial objects.

Placing each type in a single element array as follows:

type T0 = {a:number,b:number}
type T1 = {a:number,c:number,d:number}
type Test=[T0]|[T1]
const t1:Test=[{a:1,b:1,}]  
const t2:Test=[{a:1,c:1,d:1}]  
const t3:Test=[{a:1,b:1,c:1}] // fails as desired

works, but the object under test also has to be placed in a single-element array.

Is there any way to get around that?

Corsair answered 26/2, 2021 at 9:11 Comment(15)
Note that the excess properties check only applies object literals, so this would be of some utility, but only some. For instance, although let x: TA = {a:1, b:2, c:3}; will flag up the extra c property, let y = {a:1, b:2, c:3}; let x: TA = y; will not.Comparative
Excess property checks for unions seem to have been played with a couple of times (see this search), but not implemented.Comparative
You could use tagged types, but I'm guessing you'd prefer not to.Comparative
This #65806100 answer might be usefulRoderick
typescriptlang.org/…Roderick
Btw, found a Q&A using the same XOR approach I ended up with, but 2 years prior (it also offers an elegant wrapper around the exclusion logic): https://mcmap.net/q/111363/-does-typescript-support-mutually-exclusive-typesHazeghi
Actually, nevermind, I think I get what you mean - frankly, it is an oversight - glad you checked with non-homogenous property types :) Of course it should be B -> B[P] and A -> A[P], not the other way around for it to properly work when properties have different typesHazeghi
Huh, wrapping and then unwrapping members of the union? Looks nice and concise - don't see apparent drawbacks. jcalz would surely know if there are any, though.Hazeghi
@OlegValter - I just now modified it to accept the native typescript union expression, rather than multiple separate expressions. I agree with the need to check with others for drawbacks, and actually test extensively too. (BTW your simple example solution was a big help in understanding distributed conditional types).Corsair
Please don't make more work for other people by vandalizing your posts. By posting on the Stack Exchange network, you've granted a non-revocable right, under the CC BY-SA 4.0 license, for Stack Exchange to distribute that content (i.e. regardless of your future choices). By Stack Exchange policy, the non-vandalized version of the post is the one which is distributed. Thus, any vandalism will be reverted. If you want to know more about deleting a post please see: How does deleting work?Metzger
oh, wow, this got out of hand in an instant. @CraigHicks - reopen votes can take quite some time, you already have 2 out of 3, so what's the problem? Even if you did not reach a fool-proof solution, it is ok to provide it as an additional input once it is reopened. Plus, there is no harm in reopening and then closing again later (to provide others with links to relevant Q&As - this is the main benefit of the dupe closure). So, please, don't vandalize the post and let it get reviewed in due time :)Hazeghi
@Yatin - All constructive advice appreciated! My understanding was that a post that was closed can be modified and potentially reopened. I am still working on this as the question was closed under the assumption that wrapping each operand type in an array, Or'ing, and then unwrapping is not possible. (Although that assumption by the closers may have been subconscious). If the question as it appears now is the reverted version I think it is fine to reopen that.Corsair
@OlegValter - Got it! Cross posted before seeing yours.Corsair
@CraigHicks - the problem was only in the rev 10 - once you edit the closed post once, it enters the "reopen" review queue where peers vote for reopening, so there is no stopping it even if you wanted to. We generally use dupe closures for creating signposts for other users to get to time-proven Q&As where the problem is sufficiently addressed (and it seems like XORing types is at the heart of this problem). In any case, if your question stays closed - do post it as an answer to one of the targets - new solutions are always welcome (and may gain more visibility)Hazeghi
@CraigHicks - A "new answer" doesn't mean a question marked as a duplicate should be reopened. Quite the opposite. It means that a new answer should be posted to the duplicate target so that people finding any of the questions related to it are directed to a single source of canonical answers to it. Closing as a duplicate isn't a negative judgement (in most cases), just a means of ensuring others find the help they need.Comparative
H
2

The Why

In TypeScript, | is not an operator*, it denotes that the type is a union of lefthand and righthand-side types. This is important to understand if you are to grasp why {a:number,b:number} | {a:number,c:number,d:number} allows {a:number,b:number,c:number}.

When you declare a union, you tell the compiler that a type assignable to the union should be assignable to at least one member of it. With this in mind, let's check the {a:number,b:number,c:number} type from this point of view.

The lefthand side member of the union is {a:number,b:number}, which means that types assignable to it must have at least 2 properties of type number: a and b (there is a notion of excess property checks for object literals, but, as already mentioned by T.J. Crowder, this is inapplicable for unions). From the handbook**:

the compiler only checks that at least the ones required are present and match the types required

Thus, since {a:number,b:number,c:number} is assignable to {a:number,b:number} no more checks are needed - the type satisfies at least one requirement of the union. Btw, this behavior is perfectly in line with the truth table of the logical OR, which is analogous to what a union is.


Your attempt to resolve this by wrapping the types into tuples relies on naked vs. wrapped type parameter behavior. Because you wrapped the types in tuples, the compiler compares tuples of one element to each other. Obviously, the third tuple is not the same as the first and the second one, which gives you the desired result.


The What

What you actually want is the behavior exhibited by logical XOR: one of, but not both. Apart from using tagged types (mentioned by T.J. Crowder), one can define a utility transforming a pair of types into a union of "all props from A that are present in both, but not in A alone" types:

type XOR<A,B> = ({ [ P in keyof A ] ?: P extends keyof B ? A[P] : never } & B) | ({ [ P in keyof B ] ?: P extends keyof A ? B[P] : never } & A);

And here is how it would work (the trade-off of the utility is that the excess property is leaked to intellisense, but one is immediately disallowed to specify it due to never):

const t0:XOR<TA,TB> = {a:1} //property 'b' is missing
const t1:XOR<TA,TB> = {a:1,b:1} // OK
const t2:XOR<TA,TB> = {a:1,c:1,d:1} // OK 
const t3:XOR<TA,TB> = {a:1,b:1,c:1} // types of property 'c' are incompatible

Playground


* The notion of | being an operator was present in the first revision and was later edited out

** It has to be noted that this does not mean all checks shortcircuit when a match is found. In this case, all members of the union are object literals themselves, so the constraint on known properties still applies to each, leading to the TS2322 error if an unknown property is present during assignment:

const t4:XOR<TA,TB> = {a:1,b:1,c:1, g:3} //Object literal may only specify known properties
Hazeghi answered 26/2, 2021 at 9:57 Comment(5)
Your quote: "the compiler only checks that at least the ones required are present and match the types required". That's for types being passed to a function, and in that case ANY extra properties are allowed. For literal assignments (further down on the same page in the handbook) NO extra properties are allowed. What the union permits is neither one nor the other of those definitions, but appears to be a hybrid: one of union operands must satisfied by a subset of the properties, and individual additional properties are allowed if they exist in any operand. (Not described in handbook).Corsair
@CraigHicks - well, yes, you are correct, but I only wanted to emphasize that since at least one member matches, no excess property check is performed to ensure the type does not contain properties from other union members. At the same time, both members are object literals, so they cannot specify extra properties by themselves (not because they are part of the union). Does not mean it is a special case, though. Re: quote - I would say it is about interfaces, not function arguments. In any case, the statement does look like no other check is performed, so this part should be improved.Hazeghi
I agree with everything you said, and it has been a modus operandi for me from the start - not sure why you though I needed a reminder on SO dupe system, to be frank. That said, I also feel that it may be beneficial to give an asker a thorough explanation of what their situation is all about (not necessarily referring to this Q) and then point to canonical/dupeHazeghi
@OlegValter - Well, the reason is self-evident -- you posted an answer here instead of there. But whatever, these things are open to interpretation, it's always good to help people, etc. (I have serious doubts about SO's system anyway. :-) ) Happy coding!Comparative
@T.J.Crowder - would've posted there (or probably not - since it turns out that I reached the same technique as others several years before me) if I knew they are dupe targets in the first place when answering :) Q&A entropy rate on SO is horrifying. Was simply surprised to see the comment - thanks, happy coding to you too!Hazeghi

© 2022 - 2024 — McMap. All rights reserved.