Skip to content

Options

Declare options on a slash command and read them back through getters typed from what you declared. Covers the option kinds, required options dropping the null, choices, channel types, and a getter that goes missing.

Every option you declare becomes a getter on this.options, typed from what you declared. Let's take this command as an example.

src/commands/Maintenance.ts
import {
    BuilderComponent,
    RegisterCommand
} from '@seedcord/gateway';
import { ChannelType } from 'discord.js';

@RegisterCommand('global')
export class Maintenance extends BuilderComponent<'command'> {
    constructor() {
        super('command');

        this.instance
            .setName('maintenance')
            .setDescription('Post a maintenance notice')
            .addUserOption((option) =>
                option
                    .setName('notify')
                    .setDescription('Member to notify')
                    .setRequired(true)
            )
            .addChannelOption((option) =>
                option
                    .setName('target')
                    .setDescription('Channel to post in')
                    .setRequired(true)
                    .addChannelTypes(
                        ChannelType.GuildText,
                        ChannelType.GuildAnnouncement
                    )
            )
            .addStringOption((option) =>
                option.setName('reason').setDescription('Why')
            );
    }
}

The handler reads notify, target, and reason back by name.

src/handlers/Maintenance.tshover for typestap for types, arrow keys walk the tokens
@SlashRoute<"maintenance">(...routes: "maintenance"[]): <TCtor>(constructor: AssertSlashRoute<"maintenance", TCtor>) => voidSlashRoute('maintenance')
export class class MaintenanceMaintenance extends 
class SlashHandler<
    Route extends keyof SlashRegistry,
    Cache extends CacheType = CacheFor<Route>
>
SlashHandler
<'maintenance'> {
public async Maintenance.execute(): Promise<void>execute(): interface Promise<T>Promise<void> { const const notify: Usernotify = this.SlashHandler<"maintenance", "cached">.options: SlashOptions<"maintenance", "cached">options.getUser: <"notify">(name: "notify") => UsergetUser('notify'); const const target: NewsChannel | TextChanneltarget = this.SlashHandler<"maintenance", "cached">.options: SlashOptions<"maintenance", "cached">options.getChannel: <"target">(name: "target") => NewsChannel | TextChannelgetChannel('target'); const const reason: string | nullreason = this.SlashHandler<"maintenance", "cached">.options: SlashOptions<"maintenance", "cached">options.getString: <"reason">(name: "reason") => string | nullgetString('reason'); await const target: NewsChannel | TextChanneltarget.PartialTextBasedChannelFields<InGuild extends boolean = boolean>.send(options: string | MessagePayload | MessageCreateOptions): Promise<Message<true>>send(`Maintenance incoming. ${const reason: string | nullreason ?? ''}`); await this.RepliableHandler<ChatInputCommandInteraction<"cached">, Core, SentMessage, BufferResolvable | Stream | JSONEncodable<...> | Attachment | AttachmentBuilder | AttachmentPayload, ReplySender>.reply(response: string | ReplyResponse<BufferResolvable | Stream | JSONEncodable<APIAttachment> | Attachment | AttachmentBuilder | AttachmentPayload>, opts?: SendOpts): Promise<SentMessage>reply(`Notified ${const notify: Usernotify.User.username: stringusername}.`); } }

Run seedcord codegen after you add or change an option, since the handler keeps the old shape until you do.

The kinds

Each getter only accepts the names that command declared for its own kind. A typo is a compile error.

maintenance declares a user, a channel, and a string. That gives it four getters, since a user option answers both getUser and getMember.

this.options.
  • getChannel
  • getMember
  • getString
  • getUser

Each one then offers that command's options of its own kind. maintenance declared one string, so getString lists reason.

this.options.getString('
  • reason

Required options drop the null

notify and target were declared with setRequired(true), so they arrive as values. reason was left optional, so it arrives as string | null. Discord rejects the command before your handler runs when a required option is missing.

Mark an option required when your handler has nothing sensible to do without it. Discord then turns the caller away at the picker, where they can still fix it, and your handler reads the value with no guard.

const notify = this.options.getUser('notify');
const notify: User
const reason = this.options.getString('reason');
const reason: string | null
you declareyou callyou get
addStringOptiongetStringstring
addIntegerOptiongetIntegernumber, whole
addNumberOptiongetNumbernumber, decimal allowed
addBooleanOptiongetBooleanboolean
addUserOptiongetUserthe user
addUserOptiongetMemberthe member, or null
addChannelOptiongetChannelthe channel
addRoleOptiongetRolethe role
addMentionableOptiongetMentionablea user, member, or role
addAttachmentOptiongetAttachmentthe attachment

Gateway and http differ

The last six return live discord.js objects on gateway and raw discord-api-types payloads on http. Every one of them carries id on both transports. The exception is an http getMember payload, which holds the guild fields alone, since Discord sends that person's user object once under resolved.users.

getUser carries username, and getMentionable carries it when someone picked a user. A role has name, a channel has name, and an attachment has filename.

getMember returns null even for a required user option, since the person picked may have left the server.

Those are the option getters. On gateway the rest of the discord.js resolver is on this.event.options.

Choices

Adding choices narrows the return to those values.

src/commands/Probe.ts
@RegisterCommand('global')
export class Probe extends BuilderComponent<'command'> {
    constructor() {
        super('command');

        this.instance
            .setName('probe')
            .setDescription('Search the catalogue')
            .addStringOption((option) =>
                option
                    .setName('category')
                    .setDescription('Scope the query')
                    .addChoices(
                        { name: 'Books', value: 'books' },
                        { name: 'Films', value: 'films' }
                    )
            );
    }
}

The getter gives back the union of the values, so a switch over it is exhaustive.

const category = this.options.getString('category');
const category: "books" | "films" | null

Channel types

addChannelTypes narrows getChannel to the types you listed. target above allows text and announcement channels, so send is there to call.

const target = this.options.getChannel('target');
const target: NewsChannel | TextChannel

Without addChannelTypes you get every channel kind Discord might send, so send is no longer guaranteed. A forum channel doesn't have it.

Threads are the one pair you cannot split. Asking for PublicThread alone gives you PublicThread | AnnouncementThread, since discord.js types a thread's type as both at once.

When a getter is missing

A getter only exists when the command declares an option of that kind. maintenance declares no integer option, so this.options.getInteger does not exist there.

const count = this.options.getInteger('count');
Property 'getInteger' does not exist on type 'SlashOptions<"maintenance", "cached">'.

Seeing this error on a getter you believe you declared means codegen hasn't run since. Run it to update the types.