Angular ngrx union type actions are not recongnized when constructors are different

90 Views Asked by At

The following shows the actions classes and the union type. However those actions are not recognized when running the application.

export class Login implements Action {
  readonly type = LOGIN;
  constructor(
    public payload: {
      userID: string;
      email: string;
      token: string;
      expirationDate: Date;
    }
  ) {}
}

export class Logout implements Action {
  readonly type = LOGOUT;
  constructor() {}
}

export class LoginStart implements Action {
  readonly type: string = LOGIN_START;
  constructor(public payload: { username: string; password: string }) {}
}

export class LoginFail implements Action {
  readonly type: string = LOGIN_FAIL;
  constructor(public payload: string) {}
}

export type AuthActions = Login | Logout | LoginStart | LoginFail;

When running the application, ng serve gives the following error.

ERROR in src/app/auth/store/auth.reducer.ts:23:16 - error TS2339: Property 'payload' does not exist on type 'AuthActions'.
  Property 'payload' does not exist on type 'Logout'.

23         action.payload.userID,
                  ~~~~~~~
src/app/auth/store/auth.reducer.ts:24:16 - error TS2339: Property 'payload' does not exist on type 'AuthActions'.
  Property 'payload' does not exist on type 'Logout'.

24         action.payload.email,
                  ~~~~~~~
src/app/auth/store/auth.reducer.ts:25:16 - error TS2339: Property 'payload' does not exist on type 'AuthActions'.
  Property 'payload' does not exist on type 'Logout'.

25         action.payload.token,

Could someone please provide a clue on how to resolve this, without changing the constructors?

I followed following threads, but no proper solution was given. Typescript discriminated union type not recognized

2

There are 2 best solutions below

1
kfarkas On

As the error message says, the given property (payload) does not exist on Logout. As a thumb of rule in certain cases, it worths to use ActionWithPayload<T> utility type next to Action, because some actions may have payload, some may not.

This issue is with the architecture itself, not the implementation.

If you add the given utility type ActionWithPayload<T> you can use to check if the given instance has the property payload.

So first what you need is the utility interface:


interface ActionWithPayload<T = any> {
  payload: T;
}

Then you would need an utility function to check if the given object is an instance of ActionWithPayload<T>


function hasPayload(actionWithPayloadLike: any): actionWithPayloadLike is ActionWithPayload {
  return 'payload' in actionWithPayloadLike;
}

Then you can use it to check if the given instance has the proper property:


const loginFailAction: AuthActions = new LoginFail("test");
if(hasPayload(loginFailAction)) {
  alert(loginFailAction.payload);  // will show an alert
}

and


const logoutAction: AuthActions = new Logout();
if(hasPayload(logoutAction)) {
  alert(logoutAction.payload);  // wil not show an alert
}

Ofc, to prettify your code it would be awesome to implement the interface in the classes instead of Action what is misleading.

0
Viraj On

This was sorted out. The problem here was, "string" type is used for the some of the actions, but for other actions it isn't. Simply removing the type definition resolves the issue. Correction: Check the 'type' is now not 'string'.

export class LoginStart implements Action {
  readonly type = LOGIN_START;
  constructor(public payload: { username: string; password: string }) {}
}

export class LoginFail implements Action {
  readonly type = LOGIN_FAIL;
  constructor(public payload: string) {}
}