Skip to content

Several events in one class

Run one handler class for several discord.js events with this.match(), and get a compile error for any event you forget.

A member log might post when someone joins and again when someone changes their profile. Two handler classes would each need the posting code, so you'd pull it into a shared helper that both files import.

One class can answer both instead. @RegisterEvent takes as many event tuples as you pass it. this.match runs the case for the current event.

src/events/MemberLog.tshover for typestap for types, arrow keys walk the tokens
import { class EventHandler<in out Names extends ValidNonInteractionKeys>EventHandler, 
function RegisterEvent<
    const Defs extends readonly EventSpec<ValidNonInteractionKeys>[]
>(
    ...defs: Defs
): <HandlerCtor extends Constructor<EventHandler<Defs[number][0]>>>(
    constructor: HandlerCtor
) => void
RegisterEvent
} from '@seedcord/gateway';
import { enum EventsEvents, type class GuildMemberGuildMember } from 'discord.js'; @RegisterEvent<readonly [readonly [Events.GuildMemberAdd], readonly [Events.GuildMemberUpdate]]>(defs_0: readonly [Events.GuildMemberAdd], defs_1: readonly [Events.GuildMemberUpdate]): <HandlerCtor>(constructor: HandlerCtor) => voidRegisterEvent([enum EventsEvents.function (enum member) Events.GuildMemberAdd = "guildMemberAdd"GuildMemberAdd], [enum EventsEvents.function (enum member) Events.GuildMemberUpdate = "guildMemberUpdate"GuildMemberUpdate]) export class class MemberLogMemberLog extends class EventHandler<in out Names extends ValidNonInteractionKeys>EventHandler< enum EventsEvents.function (enum member) Events.GuildMemberAdd = "guildMemberAdd"GuildMemberAdd | enum EventsEvents.function (enum member) Events.GuildMemberUpdate = "guildMemberUpdate"GuildMemberUpdate > { public async MemberLog.execute(): Promise<void>execute(): interface Promise<T>Promise<void> { await this.EventHandler<Events.GuildMemberAdd | Events.GuildMemberUpdate>.match<void>(cases: EventMatchCases<Events.GuildMemberAdd | Events.GuildMemberUpdate, void>): Promise<void>match({ [enum EventsEvents.function (enum member) Events.GuildMemberAdd = "guildMemberAdd"GuildMemberAdd]: (member: GuildMembermember) => this.MemberLog.post(member: GuildMember, what: string): Promise<void>post(member: GuildMembermember, 'joined'), [enum EventsEvents.function (enum member) Events.GuildMemberUpdate = "guildMemberUpdate"GuildMemberUpdate]: (_before: GuildMember | PartialGuildMember_before, after: GuildMemberafter) => this.MemberLog.post(member: GuildMember, what: string): Promise<void>post(after: GuildMemberafter, 'updated their profile') }); } private async MemberLog.post(member: GuildMember, what: string): Promise<void>post( member: GuildMembermember: class GuildMemberGuildMember, what: stringwhat: string ): interface Promise<T>Promise<void> { await member: GuildMembermember.GuildMember.guild: Guildguild.Guild.systemChannel: TextChannel | nullsystemChannel?.
PartialTextBasedChannelFields<true>.send(
    options: string | MessagePayload | MessageCreateOptions
): Promise<Message<true>>
send
(
`${member: GuildMembermember} ${what: stringwhat}.` ); } }

You write the events twice. MemberLog passes @RegisterEvent one tuple for each event, then lists the same two events as a union in the EventHandler generic. If the decorator and the generic list different events, the class fails to compile. Both cases call the same private post, so you write the posting code once.

Warning

this.event is never on a handler registered for several events, since the payload depends on the event's name. Read the payload through this.match there.

What a case receives

A case takes that event's own payload, spread into parameters. That's one member for guildMemberAdd and two for guildMemberUpdate, under the names discord.js uses.

await this.match({
    [Events.GuildMemberAdd]: (member) =>
        this.logger.info(`${member.displayName} joined`),
    [Events.GuildMemberUpdate]: (before, after) =>
before: GuildMember | PartialGuildMember
this.logger.info( `${before.displayName} is now ${after.displayName}` ) });

before is GuildMember | PartialGuildMember. discord.js builds before from its cache. If discord.js hadn't cached the member, before is a partial whose joinedAt, joinedTimestamp, and pending are null.

Every case is required

Without match, you'd branch with a switch on the event name. Then you add a third event to the decorator and forget its case. That event then silently does nothing. match checks the cases against the union, so a missing case fails to compile.

@RegisterEvent([Events.GuildMemberAdd], [Events.GuildMemberUpdate])
export class MemberLog extends EventHandler<
    Events.GuildMemberAdd | Events.GuildMemberUpdate
> {
    public async execute(): Promise<void> {
        await this.match({
Argument of type '{ guildMemberAdd: (member: GuildMember) => Promise<void>; }' is not assignable to parameter of type 'EventMatchCases<Events.GuildMemberAdd | Events.GuildMemberUpdate, unknown>'. Property '[Events.GuildMemberUpdate]' is missing in type '{ guildMemberAdd: (member: GuildMember) => Promise<void>; }' but required in type 'EventMatchCases<Events.GuildMemberAdd | Events.GuildMemberUpdate, unknown>'.
[Events.GuildMemberAdd]: (member) => post(member, 'joined') }); } }

The second line of that error says which case is missing, here [Events.GuildMemberUpdate]. If you write a case for an event that the generic doesn't list, that fails to compile too.

Cases return values

Often only a small part differs between events, like the wording of a log line. match returns the value from the case that ran, so each case can return only the part that differs. The shared work runs once after match.

const what = await this.match({
const what: string
[Events.GuildMemberAdd]: () => 'joined', [Events.GuildMemberUpdate]: () => 'updated their profile' }); await post(what);

what is a string. post(what) runs once for either event.

Mixed frequency

Each tuple carries its own options, so one event's handler can run once while the others run on every dispatch.

@RegisterEvent(
    [Events.GuildMemberAdd, { frequency: 'once' }],
    [Events.GuildMemberUpdate]
)

Here the join case runs for the first member and never again. The update case runs every time.

When several handlers need the same check before they run, like skipping muted members, an event middleware runs it once ahead of all of them.