Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Typescript argument type from a previous argument value

I want the notify method in the below class to have type checking on the payload argument. I cannot accomplish with what appears to be straight forward code:

type UserNotificationTypes = {
  ASSIGNED_TO_USER: {
    assignedUserId: string
  }
  MAIL_MESSAGE_SENT: {
    receiverUserId: string
  }
}

export class UserNotificationService {
  notify: <TypeKey extends keyof UserNotificationTypes>(type: TypeKey, payload: UserNotificationTypes[TypeKey]) => void = (
    type,
    payload,
  ) => {
    if (type === 'ASSIGNED_TO_USER') {
      const a = payload.assignedUserId
    }

    if (type === 'MAIL_MESSAGE_SENT') {
      const b = payload.receiverUserId
    }
  }
}

Typescript shows an error Property 'assignedUserId' does not exist on type '{ assignedUserId: string; } | { receiverUserId: string; }'. Property 'assignedUserId' does not exist on type '{ receiverUserId: string; }'.

TS Playground link

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>Solution :

Potential workarounds?

Mapped types can map each type to their payload, then we can get all of them as a union by indexing into the mapped type:

type NotifyArgs = {
    [Type in keyof UserNotificationTypes]: [type: Type, payload: UserNotificationTypes[Type]];
}[keyof UserNotificationTypes];

This will result in:

[type: "EVENT_ASSIGNED_TO_USER", payload: {
    assignedUserId: string;
}] | [type: "MAIL_MESSAGE_SENT", payload: {
    receiverUserId: string;
}]

So now you can destructure that in the declaration:

notify(...[type, payload]: NotifyArgs) {

Playground


Why doesn’t my code work? (simplified)

Generics are misleading. It’s expected that this should work but it doesn’t. Why? Well, what happens if I call it like this:

notify<"ASSIGNED_TO_USER" | "MAIL_MESSAGE_SENT">(...);

From TypeScript’s point of view, that means payload can be either { assignedUserId: string } or { receiverUserId }, and we all know how accessing a property on a union type goes (hint: not good).

So because TypeScript has foreseen this potential problem, it doesn’t let you do this.
Instead, what we can do is write out the possible arguments to the function:

notify(...[type, payload]: ["ASSIGNED_TO_USER", { assignedUserId: string }] | ["MAIL_MESSAGE_SENT", { receiverUserId: string }]) { 

Then TypeScript will know that there is no possible way to call it with "both" types. This gets redundant pretty fast, hence the mapped type to do it for us.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading