Skip to content

Tagged union common property types are not narrowed #10976

Description

@farism

TypeScript Version: 2.0.2-rc

Code

interface MovePayload {
  type: string;
  active: boolean;
}

interface MoveAction {
  kind: 'move';
  payload: MovePayload;
}

interface ChatPayload {
  type: string;
  message: string;
}

interface ChatAction {
  kind: 'chat';
  payload: ChatPayload;
}

type Action = MoveAction | ChatAction

function move(payload: MovePayload) {
  console.log(payload)
}

function reducer({ kind, payload }: Action) {
  switch (kind) {
    case "move":
      return move(payload)
  }
}

reducer({ kind: 'move', payload: { type: 'forward', active: false } })

Expected behavior:
Type of payload argument passed to move function should be MovePayload

Actual behavior:
TS2345: Argument of type 'MovePayload | ChatPayload' is not assignable to parameter of type 'MovePayload'

Of course, doing move(payload as MovePayload) works correctly.

Activity

  1. changed the title [-]Tagged union common properties are not narrowed[/-] [+]Tagged union common property types are not narrowed[/+] on Sep 18, 2016
  2. farism commented on Sep 18, 2016

    @farism
    Author

    Actually.. this is user error. I believe the issue lies with doing the destructure in the reducer, i.e.

    function reducer({ kind, payload }: Action)

    Because I haven't switched on the kind at this point, flow control analysis cannot narrow down the type of payload. The following works fine

    function reducer(action: Action) {
      switch (action.kind) {
        case "move":
          return move(action.payload)
      }
    }

    I'm wondering if there is a way to make destructuring on tagged unions a bit more ergonomic? It seems like something that would be a common pattern, and having to duplicate destructures in each case could be cumbersome.

  3. mhegazy commented on Sep 20, 2016

    @mhegazy
    Contributor

    once an object is destructed, the compiler can no longer make any assumptions about the relationships between the parts. Doing so requires data-flow analysis and alias tracking which is not trivial tasks.

  4. felixfbecker commented on May 2, 2018

    @felixfbecker
    Contributor

    This would be really nice to have. I find myself often writing type guards inside filter() callbacks

    const elements = new Observable<HTMLElement | null>()
    elements.pipe(
      filter((element): element is NonNullable<typeof element> => !!element)
    )

    which is painful if you just want to narrow the type of one property/tuple element, because you need to repeat the type of the other members and cannot use destructuring:

    const mouseOvers = new Observable<MouseEvent>()
    
    mouseOvers.pipe(
      withLatestFrom(elements),
      filter((data): data is [typeof data[0], NonNullable<typeof data[1]>] => !!data[1])
    )

    vs

    mouseOvers.pipe(
      withLatestFrom(elements),
      filter(([, element]): element is NonNullable<typeof element> => !!element)
    )
  5. locked and limited conversation to collaborators on Jul 31, 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions