Skip to content

Several events in one class

Run one handler class for several discord.js events with this.match(). Covers what each arm receives, the compile error for a missing arm, returning a value from an arm, and mixing frequencies.

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 arm for whichever event fired.

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]>, any[]>>(
    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>(arms: EventMatchArms<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}.` ); } }

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 arms 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 which one fired. Read the payload through this.match there.

What an arm receives

An arm 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 arm 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 arms against the union, so a missing arm 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 'EventMatchArms<Events.GuildMemberAdd | Events.GuildMemberUpdate, unknown>'. Property '[Events.GuildMemberUpdate]' is missing in type '{ guildMemberAdd: (member: GuildMember) => Promise<void>; }' but required in type 'EventMatchArms<Events.GuildMemberAdd | Events.GuildMemberUpdate, unknown>'.
[Events.GuildMemberAdd]: (member) => post(member, 'joined') }); } }

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

Arms return values

Often only a small part differs between events, like the wording of a log line. match returns the value from the arm that ran, so each arm 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, whichever event fired.

Mixed frequency

Each tuple carries its own options, so one event can run once while the others keep firing.

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

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

Warning

seedcord records a once run against the whole class. If one class marks two different events as once, like guildMemberAdd and guildMemberUpdate, whichever fires first marks the class as run. The other event's arm then won't run until your bot restarts. Put each run-once event in its own class.

An arm on that class without once still runs every time. Another class registered for the same event keeps its own record and follows its own frequency.

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.