Skip to content

Using switch on discriminated unions with an unrecognized discriminant #13467

Closed

Description

@jinder

TypeScript Version: 2.1.5

Code

class ActionTypes {
    public static readonly TypeA = "TypeA";
    public static readonly TypeB = "TypeB";
}

interface Action {
    type: string;
    payload?: any;
}

class ActionA implements Action {
    public readonly type = ActionTypes.TypeA;
    constructor(public payload: {}) { }
}

class ActionB implements Action {
    public readonly type = ActionTypes.TypeB;
}

declare type Actions = ActionA | ActionB;

let otherType = "something-else";

function test(action: Actions | Action) {
    switch (action.type) {
        case otherType:
            console.log("Something Else");
            break; 

        case ActionTypes.TypeB:
            console.log("ActionB");
            break;

        case ActionTypes.TypeA:
            console.log("ActionA", action.payload);
            break;  
    }
}

Expected behaviour:

The last switch statement should narrow to TypeA. It doesn't when the input parameter is of type "Actions | Action" or the otherType switch statement is there.

Actual behaviour:

Property 'payload' does not exist on type Action | ActionA | ActionB. Property payload does not exist on type ActionB.

Activity

  1. aluanhaddad commented on Jan 14, 2017

    @aluanhaddad
    Contributor

    This is expected.

    Actions | Action

    is equivalent to

    type A = { type: string, payload? };

    The type: string in the interface subsumes the rest of the union, making it impossible for the type guards to narrow from it.

    Consider using this pattern instead

    namespace ActionTypes {
      export const TypeA = "TypeA";
      export const TypeB = "TypeB";
    }
    
    class ActionA {
      readonly type = ActionTypes.TypeA;
      constructor(public payload: {}) { }
    }
    
    class ActionB {
      readonly type = ActionTypes.TypeB;
    }
    
    type Actions = ActionA | ActionB;
    
    function test(action: Actions) {
      switch (action.type) {
    
        case ActionTypes.TypeB:
          console.log("ActionB");
          break;
    
        case ActionTypes.TypeA:
          console.log("ActionA", action.payload);
          break;
        default:
          console.log("Something Else");
          break;
      }
    }
  2. jinder commented on Jan 14, 2017

    @jinder
    Author

    Aluan Haddad (@aluanhaddad) I see, thank you. I guess I don't see quite why it would subsume like that.

    Closing as this seems to be expected behaviour.

  3. aluanhaddad commented on Jan 15, 2017

    @aluanhaddad
    Contributor

    Jinder Sidhu (@jinder) The reason is basically that if we say that a type T represents either any string or respresents some specific string S we are saying that T represents any string since the specific string S is a string that can be represented by T. This makes the union "collapse". Does this sound right?

  4. rkrisztian commented on Feb 9, 2017

    @rkrisztian

    This solution is not good enough for actions in general because we need the Action interface.

    For example, find-book-page.ts dispatches an action:

    this.store.dispatch(new book.SearchAction(query));

    And book.ts needs to define an Observable<Action>.

    I guess casting would be a workaround, but having to do that everytime an Action object is needed, would be cumbersome.

  5. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions