Search code examples
reactjstypescriptreduxredux-saga

Property 'loginToken' does not exist on type '{ loginToken: string; } | { error: Error; } | { username: string; password: string; }'


I am trying to create a reducer using typescript/ redux and I am facing this error

Property 'loginToken' does not exist on type '{ loginToken: string; } | { error: Error; } | { username: string; password: string; }'.
  Property 'loginToken' does not exist on type '{ error: Error; }'

My reducer is as following:

export default (state = userState, action: UsersActions) => {
  switch (action.type) {
    case ActionTypes.USER_LOGIN_REQUEST:
      return {
        ...state,
        isAuthenticated: false,
        isLoading: true,
      };
    case ActionTypes.USER_LOGIN_SUCCESS:
      return {
        ...state,
        isAuthenticated: true,
        loginToken: action.payload.loginToken,
        isLoading: false,
      };

My action types:

export interface UserLoginRequest extends Action {
  type: typeof ActionTypes.USER_LOGIN_REQUEST; // !! here we assign the string literal type of the constant
  payload: {
    username: string;
    password: string;
  };
}

export interface UserLoginSuccess extends Action {
  type: typeof ActionTypes.USER_LOGIN_SUCCESS; // !! here we assign the string literal type of the constant
  payload: {
    loginToken: string;
  };
}

export interface UserLoginFailure extends Action {
  type: typeof ActionTypes.USER_LOGIN_FAILURE; // !! here we assign the string literal type of the constant
  payload: {
    error: Error;
  };
}

export interface User {
  username: string;
  password: string;
  loginToken: string;
  loginError?: Error;
  status?: number;
}

export type UsersActions = 
| UserLoginSuccess
| UserLoginFailure
| UserLoginRequest;

Action types are an enum like this:

enum ActionTypes {
    USER_LOGIN_REQUEST = 'request',
    USER_LOGIN_SUCCESS = 'success',
    USER_LOGIN_FAILURE = 'failure',
};

Maybe I am not getting something regarding Typescript so that's why? if any resources or insights can be provided that would be appreciated.


Solution

  • The problem is that UsersActions can be one of three things - a UserLoginSuccess, a UserLoginFailure, or a UserLoginRequest - it's a Union Type.

    In the "Success" case, you know that the login token will exist, and that the type is a UserLoginSuccess - but TypeScript doesn't have any way of knowing this, so it's telling you that you may be trying to get the loginToken property from (e.g.) a UserLoginFailure type.

    The solution is that you've got to narrow the type when you want to work with it. Since there's no common properties on your union type that you could switch on, the only way I can think to do this would be to cast the payload to the correct type:

    case ActionTypes.USER_LOGIN_SUCCESS:
      return {
        ...state,
        isAuthenticated: true,
        loginToken: (action.payload as UserLoginSuccess).loginToken,
        isLoading: false,
      };
    

    Edit

    Sorry, I just re-read your question and noticed that there's a better solution - when you're creating your union types, you're using typeof for the action type -

    export interface UserLoginRequest extends Action {
      type: typeof ActionTypes.USER_LOGIN_REQUEST;
      // ...
    }
    

    So type will be the type of whatever that property is - but I'm guessing what you actually want to do is make it into a known value?

    I don't know what ActionsTypes is defined as, but if this was an enum -

    enum ActionTypes {
        USER_LOGIN_REQUEST = 'request',
        USER_LOGIN_SUCCESS = 'success',
        USER_LOGIN_FAILURE = 'failure',
    };
    

    Then you could change the type definitions to use the enum type instead -

    interface UserLoginRequest extends Action {
      type: ActionTypes.USER_LOGIN_REQUEST;
      payload: {
        username: string;
        password: string;
      };
    }
    

    Now, when you switch on the type, TS will correctly narrow the payload for you -

    switch (action.type) {
        case ActionTypes.USER_LOGIN_SUCCESS:
            console.log(action.payload.loginToken); // TS is happy!
            break;
    
        default:
            console.log('NOT A SUCCESS ACTION');
    }
    

    Here's a quick repro in TS playground