Flow: Disjoint Unions don't work as state for React components

Created on 4 Feb 2017  路  7Comments  路  Source: facebook/flow

I'm having an issue with using disjoint unions directly as state for a react component. This does not typecheck correctly:

type AppState = LoggedInState | NotLoggedInState;
type LoggedInState = {mode: "loggedIn"; username: string};
type NotLoggedInState = {mode: "notLoggedIn"};

class App extends Component<any, any, AppState> {
  state: AppState;
  constructor(props: any) {
    super(props);
    this.state = {mode: "notLoggedIn"};
  }
  setLoggedIn() {
    this.setState({mode: "loggedIn", username: "test"});
  }
  render() {
    if (this.state.mode === 'notLoggedIn') {
      return <Login loginFunc={this.setLoggedIn()}/>
    } else {
      return <Dashboard/>
    }
  }
}

The errors I get are:

src/Components/App.js:19                                                                                                                                                                                                                                  
 19:     this.setState({mode: "loggedIn", username: "test"});                                                                                                                                                                                             
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `setState`                                                                                                                                                                    
 19:     this.setState({mode: "loggedIn", username: "test"});                                                                                                                                                                                             
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ property `username` of object literal. Property not found in                                                                                                                                  
 12: class App extends Component<any, any, AppState> {                                                                                                                                                                                                    
                                           ^^^^^^^^ object type                                                                                                                                                                                           

src/Components/App.js:19                                                                                                                                                                                                                                  
 19:     this.setState({mode: "loggedIn", username: "test"});                                                                                                                                                                                             
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `setState`                                                                                                                                                                    
 19:     this.setState({mode: "loggedIn", username: "test"});                                                                                                                                                                                             
                              ^^^^^^^^^^ string. Expected string literal `notLoggedIn`, got `loggedIn` instead                                                                                                                                            
  8: type NotLoggedInState = {mode: "notLoggedIn"};                                                                                                                                                                                                       
                                    ^^^^^^^^^^^^^ string literal `notLoggedIn`                                                                                                                                                                            

Found 2 errors

However if I encapsulate AppState into an intermediate object:

type State = LoggedInState | NotLoggedInState;
type LoggedInState = {mode: "loggedIn"; username: string};
type NotLoggedInState = {mode: "notLoggedIn"};

type AppState = {state: State}

class App extends Component<any, any, AppState> {
  state: AppState;
  constructor(props: any) {
    super(props);
    this.state = {state: {mode: "notLoggedIn"}};
  }
  setLoggedIn() {
    this.setState({state: {mode: "loggedIn", username: "test"}});
  }
  render() {
    if (this.state.mode === 'notLoggedIn') {
      return <Login loginFunc={this.setLoggedIn}/>
    } else {
      return <Dashboard/>
    }
  }
}

It typechecks correctly with no errors.

I'm having trouble figuring out if this is a bug in Flow or a bug in React (possibly related to how type parameters are used in its code?) however adding this issue is probably a good starting point.

Pending clarification object model unionintersections bug react

Most helpful comment

Simple example:

type A = { t: 'A' };
type B = { t: 'B' };

type C = A | B;

var a: A = { t: 'A' };
var c: C = a;

c.t = 'B'; // can't do that

All 7 comments

I'm also experiencing this and wondering the same.

The possible bug here is that $Shape doesn't work on disjoint unions.

This is actually expected, because setState mutates the state and you can't mutate union type

Simple example:

type A = { t: 'A' };
type B = { t: 'B' };

type C = A | B;

var a: A = { t: 'A' };
var c: C = a;

c.t = 'B'; // can't do that

Why does it work if I'm modifying an encapsulating object? Additionally, I'm not setting mode directly - rather, I'm replacing the object.

@vkurchatkin Shouldn't the error be on the var c: C = a line. The objects are not marked covariant.

No, it shouldn't. X is always a subtype of X | Y, whatever Y is.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

doberkofler picture doberkofler  路  3Comments

cubika picture cubika  路  3Comments

ctrlplusb picture ctrlplusb  路  3Comments

jamiebuilds picture jamiebuilds  路  3Comments

ghost picture ghost  路  3Comments