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

Intersection type operator doesn't discriminate Union Types #13203

Closed
nahuel opened this issue Dec 28, 2016 · 9 comments
Closed

Intersection type operator doesn't discriminate Union Types #13203

nahuel opened this issue Dec 28, 2016 · 9 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@nahuel
Copy link

nahuel commented Dec 28, 2016

type A = {tag: 'a', a: 1} | {tag: 'b', b: 1}
type B = A & {tag: 'b'}

declare var b : B
let z = b.b    // Error: Property 'b' does not exist on type 'B'

Expected behavior: b (type B) only possible value for tag property is 'b', so it must be discriminated to {tag : 'b' , 'b' : 1} without needing to enclose it in a if(b.tag == 'b') type guard
Actual behavior: b (type B) is not discriminated, needs type guards just like plain A
TypeScript Version: 2.0.6

@mhegazy
Copy link
Contributor

mhegazy commented Dec 28, 2016

type B (after distributing the intersection into the union) is { tag: 'a' & 'b', a: 1} | {tag: 'b', b: 1}. this type is not guaranteed to have a property b on it.

if i understand correctly, what you want is:

type A = {tag: 'a', a: 1} | B;
type B = {tag: 'b', b: 1};

declare var b : B
let z = b.b   

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Dec 28, 2016
@nahuel
Copy link
Author

nahuel commented Dec 29, 2016

The type { tag: 'a' & 'b', a: 1} | {tag: 'b', b: 1} you mention can guarantee tag is always 'b'. The first part of the type {tag : 'a' & 'b', a: 1} has an EMPTY set of possible values, because NO value can satisfy 'a' & 'b' at the same time, so the only possible subset of values are the ones in the other term of the type expression, {tag : 'b', b : 1}. By using that deduction TS can discriminate the union in my example.

The TS specs says:

Intersection types represent values that simultaneously have multiple types. A value of an intersection type A & B is a value that is both of type A and type B.

To clarify, in this code:

type A = {tag: 'a', a: 1} 
type B = {tag: 'b', b: 1}
type TaggedAsB = {tag: 'b'}

The (A | B) & TaggedAsB intersection type only possible values are the ones pertaining to B. TS currently doesn't know this, but the intersection type is unambiguously discriminating the union.

I think this is not a TS bug, but it can be seen as a current shortcoming and as a nice feature to have. Maybe TS should collapse the {tag: 'a' & 'b', a: 1 } type to the never type by detecting 'a' & 'b' has no possible values. Then it can reach the discriminated union case by doing the following reductions:

1- (B | A) & TaggedAsB
2- ({tag : 'b', b : 1} | {tag: 'a' , a: 1}) & {tag: 'b'}
3- {tag : 'b', b : 1}  | {tag: 'a' & 'b', a: 1}
4- {tag : 'b', b : 1}  | never
5- {tag : 'b', b : 1}  // Discriminated Union case

Note, the sample you give is not what I want (and note, you didn't use A). I want to use an intersection type to specialize an existing union type to a discriminated case.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 29, 2016

has an EMPTY set of possible values, because NO value can satisfy 'a' & 'b' at the same time, so the only possible subset of values are the ones in the other term of the type expression, {tag : 'b', b : 1}

Having a type with no values is fine. it is not an error to have a type that does not have any values. e.g. number & string is a fine type; and it is not never.

if the intention here is to cast, i would cast to B directly.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 29, 2016

I should add, it is intentional that it is not an error, and it is intentional that intersection of primitives and/or literals do not reduce to never.

@nahuel
Copy link
Author

nahuel commented Dec 29, 2016

Then the feature request can be restated as this:

Make TS discriminate/narrow an union when one or more of his constituent types have no possible values, but without reducing them to the never type. So let x : { tag: 'a' & 'b', a: 1} | {tag: 'b', b: 1} will be exactly equivalent to let x : {tag: 'b', b: 1} without using a type guard. I don't see a reason to not take advantage of knowing a type from a DU has no possible values to avoid writing type guards.

To show you why I don't want to use castings, here is my use case:

type Event = ({eventName : "EVENT_A", dataA : any} | 
              {eventName : "EVENT_B", dataB : any})

// define a callback for an specific event, use an intersection type operator to select
// a case from the Event DU.
function handlerEventA(event : Event & {eventName: "EVENT_A"}) {
       // with this new feature, here we don't need a typeguard, TS knows
       // what specific DU case is event:
       let x = event.dataA 
}

I want to avoid the need of defining a type for each event name. By having the possibility of choosing a case in a discriminating union using an intersection type, this can be easily resolved and the handlerEventA callback signature will be correctly typed.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 29, 2016

I am not sure i see the reason behind writing an intersection type in these locations. writing

function handlerEventA(event : {eventName : "EVENT_A", dataA : any}) {

seems to be as much work as Event & {eventName: "EVENT_A"}, but way more readable.

@nahuel
Copy link
Author

nahuel commented Dec 29, 2016

If you do that, then you must replicate the entire specific event type definition in each possible handler, which can be huge, or assign a type name for each one of them (what I want to avoid).

I think this can be useful in many other cases. To be more specific, the feature request is:

Given a Discriminated Union type, make possible to create a new type that narrows to an specific case of the DU (creating it at the static type level, without using runtime typeguards).
One way is to add to TS the intelligence to discriminate an union by detecting types in it with no possible values and then using the Intersection Type operator to narrow the DU.

@nahuel
Copy link
Author

nahuel commented Dec 29, 2016

PD: Maybe it will be better to change the title of this issue to "make possible to narrow Discriminated Unions statically" and add the feature request label.

@nahuel
Copy link
Author

nahuel commented Jan 5, 2017

please close this issue, I re-stated it in a more clear form at #13300

@mhegazy mhegazy closed this as completed Jan 5, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants