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; }'.
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) {
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.