Home > Net >  How to fix "Property not exist error" with Discriminating Unions types?
How to fix "Property not exist error" with Discriminating Unions types?

Time:05-04

I'm trying to refactor a project in TS (still learning the basics, sry) and I set up the typing of the authentication user roles in this way:

export interface Guest {
  isGuest: true;
  isManager: false;
  isAdmin: false;
}

export interface LoggedUser {
  firstName: string;
  lastName: string;
  isActive: boolean;
  email: string;
  userId: number;
  username: string;
  isManager: boolean;
  isGuest: false;
  isAdmin: boolean;
}

export type User = Guest | LoggedUser;

I then set up a React Context and a hook for getting user information throughout the application. The hook retrieves all the info (name, last name, email, etc) correctly but then I have a type error.

const user = useUser();
const { email } = user;

When I try to use properties of the logged user, TypeScript still check for both types even if I set them up as conditional (one OR the other) I get this error:

Property 'email' does not exist on type 'User'.
  Property 'email' does not exist on type 'PublicUser'.

I've found a sort of workaround with a simple if statement, but I don't get why the compiler doesn't accept the discriminating union.

CodePudding user response:

The compiler is smart enough to discriminate between the two, you just have to actually check the field that's doing the discriminating, i.e. isGuest:

const user = useUser()

if (user.isGuest) {
  // can access isGuest, isManager, isAdmin here
} else {
  // can access all the LoggedUser properties here
}

It's up to you what your component actually does when it doesn't have an email, but from the types you've listed, that looks like a valid situation.

Edit:

I've found a sort of workaround with a simple if statement, but I don't get why the compiler doesn't accept the discriminating union.

This is not a workaround, it's the point of Typescript: something that's a User type might have an email, but it also might not, because it's a Guest. Typescript makes you actually check, instead of assuming.

CodePudding user response:

Typescript is working fine. If you define multiple return types and you don't cast the result to any specific type, Typescript will assume that the given result's safe properties are the ones that are available in both types.

You can set the type to LoggedUser:

const user = useUser();
const { email } = user as LoggedUser;
// email will be value or null

Another approach is to define differently your types to avoid setting the return type on every function call (I prefere type notation instead of interface):

export type CommonInfo = {
  isGuest: boolean;
  isManager: boolean;
  isAdmin: boolean;
};

export type LoggedOnly = {
  firstName: string;
  lastName: string;
  isActive: boolean;
  email: string;
  userId: number;
  username: string;
};

export type Guest = CommonInfo & Partial<{ [K in keyof LoggedOnly]: never }>;
// or: export type Guest = CommonInfo & Partial<{ [K in keyof LoggedOnly]: null }>;
// or: export type Guest = CommonInfo & Partial<LoggedOnly>;

export type LoggedUser = CommonInfo & LoggedOnly;
export type User = Guest | LoggedUser;

That way you will avoid the type error when executing const { email } = useUser();.

I hope this answer helps.

  • Related