Casper
Casper

Reputation: 1

Flow complaining about action union type in reducer

Flow throws 3 errors (property not found) for each parameter (action.location, action.weatherResult and action.error). The only solution I found is to not union and have just one action type with the 3 different properties as optional maybes but the properties aren't optional so it doesn't solve my problem.

Actions

// @flow
import actionTypes from './index';    

export type FetchWeatherStartAction = {
  type: string,
  location: string
};    

export type FetchWeatherSuccessAction = {
  type: string,
  weatherResult: ?string
};    

export type FetchWeatherFailAction = {
  type: string,
  error: string | false
};    

export type WeatherAction = FetchWeatherStartAction | FetchWeatherSuccessAction | FetchWeatherFailAction;    

const fetchWeatherStart = (location: string): FetchWeatherStartAction => ({
  type: actionTypes.WEATHER_FETCH_START,
  location
});    

const fetchWeatherSuccess = (weatherResult: ?string): FetchWeatherSuccessAction => ({
  type: actionTypes.WEATHER_FETCH_SUCCESS,
  weatherResult
});    

const fetchWeatherFail = (error: string | false): FetchWeatherFailAction => ({
  type: actionTypes.WEATHER_FETCH_FAIL,
  error
});    

export {
  fetchWeatherStart,
  fetchWeatherSuccess,
  fetchWeatherFail
}

Action Types

// @flow
const actionTypes = {
  WEATHER_FETCH_START: 'WEATHER_FETCH_START',
  WEATHER_FETCH_SUCCESS: 'WEATHER_FETCH_SUCCESS',
  WEATHER_FETCH_FAIL: 'WEATHER_FETCH_FAIL'
}    

export default actionTypes;

Reducer

// @flow
import actionTypes from './../actions';
import type { WeatherAction } from './../actions/weather';    

/*export type WeatherActionType = {
  type: string,
  error?: boolean | string,
  weatherResult?: string | null,
  location?: string
};*/    

export type WeatherStateType = {
  location: string,
  fetchedFromServer: boolean,
  isFetching: boolean,
  fetchError: boolean | string,
  weatherResult: ?string
};    

const defaultState: WeatherStateType = {
  location: 'Barcelona',
  fetchedFromServer: false,
  isFetching: false,
  fetchError: false,
  weatherResult: null
};    

const weather = (state: WeatherStateType = defaultState, action: WeatherAction): WeatherStateType => {    

  switch (action.type) {    

    case actionTypes.WEATHER_FETCH_START:
      return {
        ...state,
        isFetching: true,
        fetchError: false,
        location: action.location
      };    

    case actionTypes.WEATHER_FETCH_SUCCESS:
      return {
        ...state,
        fetchedFromServer: true,
        isFetching: false,
        fetchError: false,
        weatherResult: action.weatherResult
      };    

    case actionTypes.WEATHER_FETCH_FAIL:
      return {
        ...state,
        fetchedFromServer: false,
        isFetching: false,
        fetchError: action.error
      };    

    default:
      return state;
  }    

};    

export default weather;

Upvotes: 0

Views: 350

Answers (2)

JSNoob
JSNoob

Reputation: 1577

In case anyone stumble upon this problem.

Root cause: Flow will remove all refinements to the types when you do a function call.

Solution: Access the param before you make the function call

For example, in your case, the reducer can be written in this way:

const reducer = (state: {}, action: WeatherAction): WeatherStateType => {
    switch (action.type) {
      case actionTypes.WEATHER_FETCH_START:
          const {location} = action;
          return { ...state, isFetching: true, fetchError: false, location: location};
      case actionTypes.WEATHER_FETCH_SUCCESS:
          const {weatherResult} = action;
          return {...state, fetchedFromServer: true, isFetching: false, fetchError: false,, weatherResult: weatherResult};
      case actionTypes.WEATHER_FETCH_FAIL:
          const {error} = action;
          return {...state. fetchedFromServer: false, isFetching: false, fetchError: action.error};
      default: 
          return state
    }
}

Upvotes: 0

Peter Hall
Peter Hall

Reputation: 58715

You are attempting to rely on type information that is not actually encoded in your types.

For example in the definition of FetchWeatherStartAction:

export type FetchWeatherStartAction = {
  type: string,
  location: string
};

type is declared to be a string. Any string at all.

But later, in this switch case:

switch (action.type) {    
    case actionTypes.WEATHER_FETCH_START:
        ...
        action.location
        ...

You are expecting Flow to know that FetchWeatherStartAction is the only possible alternative of the WeatherAction enum which could have 'WEATHER_FETCH_START' as the value of its type property. Based on the types alone, any action could have any value for its type. The only thing we can be sure about is that it's a string.

The solution is to define your action variants to have more specific types, which incorporate their legal values.

export type FetchWeatherStartAction = {
  type: 'WEATHER_FETCH_START',
  location: string
};    

export type FetchWeatherSuccessAction = {
  type: 'WEATHER_FETCH_SUCCESS',
  weatherResult: ?string
};    

export type FetchWeatherFailAction = {
  type: 'WEATHER_FETCH_FAIL',
  error: string | false
};  

When you check that type === 'WEATHER_FETCH_START', Flow can be certain that the actual type is FetchWeatherStartAction. This is possible because it already knows that action is a WeatherAction and WeatherAction is an enum with only those three possible values.

It's a little unfortunate that you have to repeat the string literals, rather than refer to a constant. I know people get uneasy about that, but I would argue in this case that all of the reasons why magic constants are considered bad practice are dealt with by Flow's type checking. In Javascript, using a syntactic identifier to access a field is semantically no different than accessing it by its string name.

Upvotes: 3

Related Questions