Home > Enterprise >  Typescript argument type from a previous argument value
Typescript argument type from a previous argument value

Time:09-21

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

CodePudding user response:

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.

  • Related