router: support all command/option options, nicer component builders
@@ -11,6 +11,8 @@ - [adding subcommands](#adding-subcommands)
- [context menus](#context-menus) - [interactive components](#interactive-components) - [adding autocomplete](#adding-autocomplete) +- [static choices](#static-choices) +- [localisation & other available configurations](#localisation--other-available-configurations) - [using middleware](#using-middleware) - [using afterware](#using-afterware) - [handling errors](#handling-errors)@@ -158,10 +160,15 @@ // ticketId: strign | undefined
await interaction.reply(`closing ticket: ${ticketId}`); }, }); +``` -const button = new ButtonBuilder() +instead of instantiating standard discord.js builders and setting their custom IDs manually, you can use the built-in type-safe builders directly on your component instance: + +```ts +const button = closeTicketButton + .button({ ticketId: "67" }) // customId serialises to `close-ticket:67` .setLabel("close ticket") - .setCustomId(closeTicketButton.id({ ticketId: "67" })); // serialises to `close-ticket:67` + .setStyle(ButtonStyle.Danger); ``` kitten will automatically match incoming interactions to the `close-ticket`@@ -170,11 +177,9 @@
modals and select menus work similarly. ```ts -const modal = new ModalBuilder() // - .setCustomId(myModal.id({ foo: "bar" })); // serialises to `my-modal:bar` +const modal = myModal.modal({ foo: "bar" }); // customId serialises to `my-modal:bar` -const selectMenu = new StringSelectMenuBuilder() // - .setCustomId(mySelectMenu.id({ foo: "baz" })); // serialises to `my-select-menu:baz` +const selectMenu = mySelectMenu.selectMenu({ foo: "baz" }); // id = `my-select-menu:baz` ``` note! custom IDs must be 100 characters or less, so be mindful of how much data@@ -211,6 +216,67 @@ value: option.string.required("the value"),
}, async run(interaction, { key, value }) { await interaction.reply(`set ${key} to ${value}`); + }, +}); +``` + +## static choices + +in addition to async `autocomplete` functions, command options can define static +choice lists directly. Discord will then present these choices to the user as a +dropdown list. + +```ts +const dbDriverOption = option.string("database driver", { + required: true, + choices: [ + { name: "PostgreSQL", value: "postgres" }, + { name: "SQLite", value: "sqlite" }, + ], +}); +``` + +## localisation & other available configurations + +kitten supports multi-locale configuration, NSFW toggles, and member permission +definitions across options, choices, top-level commands, subcommands, and +subcommand groups. + +```ts +import { PermissionFlagsBits } from "discord.js"; + +const configCommand = kitten.command("config", { + description: "configure bot settings", + nameLocalizations: { + fr: "configuration", + "es-ES": "ajustes", + }, + defaultMemberPermissions: PermissionFlagsBits.Administrator, + nsfw: false, +}); + +configCommand.subcommand("set", { + description: "set a configuration value", + nameLocalizations: { + fr: "definir", + }, + options: { + key: option.string("the setting key", { + required: true, + nameLocalizations: { + fr: "clef", + }, + choices: [ + { + name: "Prefix", + value: "prefix", + nameLocalizations: { fr: "Préfixe" }, + }, + ], + }), + }, + async run(interaction, { key }) { + await interaction.reply(`setting updated: ${key}`); }, }); ```
@@ -0,0 +1,31 @@
+# [1.7.0] 2026-07-09 + +in this release i introduced friendlier ui component builders, static option +choices, and added support for localisation, permissions, and nsfw configurations. + +to make UI creation more straightforward, `KittenComponent` now provides type-safe +helpers to generate pre-configured discord.js builders directly. calling these +helpers automatically serializes your arguments and custom ids behind the scenes: + +- `.button()` on button components, +- `.stringSelectMenu()`, `.userSelectMenu()`, `.roleSelectMenu()`, `.channelSelectMenu()`, and `.mentionableSelectMenu()` on select menus, and +- `.modal()` on modal components. + +i've expanded support for command registration configurations to align with the discord API: + +- `nameLocalizations` and `descriptionLocalizations` are now configurable on options, + choices, top-level commands, subcommands, and subcommand groups, +- `defaultMemberPermissions` accepts any discord.js permission bitfield, string, + or bigint and safely translates them to the API payload during synchronization, and +- `nsfw` toggles can be added to top-level commands or subcommand builders. + +command options can also now use static choices. + +you may like to know that the difference in the British and American spellings of +"localisation" and "localization" is intentional has fooled me several times during +development of this. + +you can find more information and examples under the "interactive components", "static choices", +and "localisation & advanced configuration" sections of the [Getting Started] guide. + +[Getting Started]: ../GET-STARTED.md
@@ -1,6 +1,6 @@
{ "name": "@purrkit/router", - "version": "1.6.2", + "version": "1.7.0", "license": "EUPL-1.2", "author": { "name": "apr",
@@ -6,6 +6,7 @@ BaseInteraction,
ButtonInteraction, AnySelectMenuInteraction, ModalSubmitInteraction, + LocalizationMap, } from "discord.js"; import type { CommandOption, Option, InferOptions } from "./options"; import { KittenComponent } from "./components";@@ -47,7 +48,6 @@ interaction: I,
ctx: Ctx, ) => Promise<unknown> | unknown; -// Parameterized with Res. result is Res | undefined because execution may fail/halt. export type Afterware<I extends BaseInteraction = BaseInteraction, Ctx = any, Res = unknown> = ( interaction: I, ctx: Ctx,@@ -60,6 +60,8 @@ constructor(
public name: string, public config: { description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; options?: Record<string, CommandOption<any, any>>; onError?: ErrorHandler<ChatInputCommandInteraction, any>; run: Function;@@ -73,12 +75,18 @@
constructor( public name: string, public description: string, + public config?: { + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ) {} subcommand<O extends Record<string, CommandOption<any, any>>, R extends Res>( name: string, config: { description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; options?: O; onError?: ( err: unknown,@@ -110,6 +118,10 @@ public afterwares: Afterware<any, any, any>[] = [],
public config?: { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; }, ) {}@@ -117,6 +129,8 @@ subcommand<O extends Record<string, CommandOption<any, any>>, R extends Res>(
name: string, config: { description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; options?: O; onError?: ( err: unknown,@@ -136,10 +150,14 @@ }
group( name: string, - config: { description: string }, + config: { + description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, setup: (group: SubcommandGroup<Ctx, Res>) => void, ): this { - const group = new SubcommandGroup<Ctx, Res>(name, config.description); + const group = new SubcommandGroup<Ctx, Res>(name, config.description, config); setup(group); this.groups[name] = group; return this;@@ -154,6 +172,10 @@ description: string;
options?: Record<string, CommandOption<any, any>>; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<ChatInputCommandInteraction, Ctx>; run: ( interaction: ChatInputCommandInteraction,@@ -182,6 +204,9 @@ public afterwares: Afterware<any, any, any>[] = [],
public config?: { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<I, Ctx>; }, ) {}@@ -212,7 +237,6 @@ this.afterwares,
); } - // Refines the Res generic type parameter of the CommandBuilder after<NewRes>(handler: Afterware<BaseInteraction, Ctx, NewRes>): CommandBuilder<Ctx, NewRes> { return new CommandBuilder<Ctx, NewRes>(this.middlewares, this.errorHandlers, [ ...this.afterwares,@@ -227,6 +251,10 @@ description: string;
options?: O; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<ChatInputCommandInteraction, Ctx>; run: ( interaction: ChatInputCommandInteraction,@@ -242,6 +270,10 @@ config: {
description: string; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; }, ): SubcommandGroupBuilder<Ctx, Res>;@@ -264,6 +296,10 @@ this.afterwares,
{ integrationTypes: config.integrationTypes, contexts: config.contexts, + nameLocalizations: config.nameLocalizations, + descriptionLocalizations: config.descriptionLocalizations, + defaultMemberPermissions: config.defaultMemberPermissions, + nsfw: config.nsfw, }, ); }@@ -275,6 +311,9 @@ | ((interaction: UserContextMenuCommandInteraction, ctx: Ctx) => CommandResultType<R>)
| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<UserContextMenuCommandInteraction, Ctx>; run: (interaction: UserContextMenuCommandInteraction, ctx: Ctx) => CommandResultType<R>; },@@ -300,6 +339,9 @@ this.afterwares,
{ integrationTypes: runOrConfig.integrationTypes, contexts: runOrConfig.contexts, + nameLocalizations: runOrConfig.nameLocalizations, + defaultMemberPermissions: runOrConfig.defaultMemberPermissions, + nsfw: runOrConfig.nsfw, onError: runOrConfig.onError, }, );@@ -312,6 +354,9 @@ | ((interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => CommandResultType<R>)
| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<MessageContextMenuCommandInteraction, Ctx>; run: ( interaction: MessageContextMenuCommandInteraction,@@ -340,6 +385,9 @@ this.afterwares,
{ integrationTypes: runOrConfig.integrationTypes, contexts: runOrConfig.contexts, + nameLocalizations: runOrConfig.nameLocalizations, + defaultMemberPermissions: runOrConfig.defaultMemberPermissions, + nsfw: runOrConfig.nsfw, onError: runOrConfig.onError, }, );
@@ -1,7 +1,14 @@
-import type { - ButtonInteraction, - AnySelectMenuInteraction, - ModalSubmitInteraction, +import { + type ButtonInteraction, + type AnySelectMenuInteraction, + type ModalSubmitInteraction, + ButtonBuilder, + StringSelectMenuBuilder, + UserSelectMenuBuilder, + RoleSelectMenuBuilder, + ChannelSelectMenuBuilder, + MentionableSelectMenuBuilder, + ModalBuilder, } from "discord.js"; import type { Option, InferOptions } from "./options"; import type { Middleware, ErrorHandler, Afterware } from "./commands";@@ -16,7 +23,7 @@ export class KittenComponent<
I extends KittenComponentInteraction = KittenComponentInteraction, O extends Record<string, Option<any, any>> = Record<string, Option<any, any>>, Ctx = any, - Res = unknown, // Added Res generic + Res = unknown, > { type = "component" as const;@@ -25,7 +32,7 @@ public name: string,
public config: { options?: Record<string, Option<any, any>>; onError?: ErrorHandler<I, Ctx>; - run: (interaction: I, args: InferOptions<O>, ctx: Ctx) => Promise<Res> | Res; // Typed run return + run: (interaction: I, args: InferOptions<O>, ctx: Ctx) => Promise<Res> | Res; }, public middlewares: Middleware<any, any, any>[] = [], public errorHandlers: ErrorHandler<any, any>[] = [],@@ -66,5 +73,55 @@ else parsed[key] = val === "" ? undefined : val;
}); return parsed as InferOptions<O>; + } + + button( + this: KittenComponent<ButtonInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): ButtonBuilder { + return new ButtonBuilder().setCustomId(this.id(args as any)); + } + + stringSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): StringSelectMenuBuilder { + return new StringSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + userSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): UserSelectMenuBuilder { + return new UserSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + roleSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): RoleSelectMenuBuilder { + return new RoleSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + channelSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): ChannelSelectMenuBuilder { + return new ChannelSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + mentionableSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): MentionableSelectMenuBuilder { + return new MentionableSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + modal( + this: KittenComponent<ModalSubmitInteraction, O, Ctx, Res>, + title: string, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): ModalBuilder { + return new ModalBuilder().setTitle(title).setCustomId(this.id(args as any)); } }
@@ -16,4 +16,4 @@ // - [x] slash commands
// // other // - [ ] banish `any` in as many places as possible -// - [ ] custom error handlers +// - [x] custom error handlers
@@ -13,6 +13,8 @@ type MessageContextMenuCommandInteraction,
BaseInteraction, type RESTPostAPIApplicationCommandsJSONBody, ApplicationCommandType, + PermissionsBitField, + type LocalizationMap, } from "discord.js"; import { HaltExecution } from "./errors";@@ -43,7 +45,6 @@ error: [message: string, data?: Record<string, any>];
} export class Kitten extends EventEmitter<KittenEvents> { - // Mapped to any/any to support multiple different return types in a single instance private commands = new Map<string, Command<any, any> | SubcommandGroupBuilder<any, any>>(); private userContextMenus = new Map<string, ContextMenuCommand<any, any, any>>(); private messageContextMenus = new Map<string, ContextMenuCommand<any, any, any>>();@@ -79,6 +80,10 @@ description: string;
options?: O; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<ChatInputCommandInteraction, {}>; run: (interaction: ChatInputCommandInteraction, args: InferOptions<O>) => CommandResultType; },@@ -90,6 +95,10 @@ config: {
description: string; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; }, ): SubcommandGroupBuilder<{}>;@@ -104,6 +113,9 @@ | ((interaction: UserContextMenuCommandInteraction) => CommandResultType)
| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<UserContextMenuCommandInteraction, {}>; run: (interaction: UserContextMenuCommandInteraction) => CommandResultType; },@@ -118,6 +130,9 @@ | ((interaction: MessageContextMenuCommandInteraction) => CommandResultType)
| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<MessageContextMenuCommandInteraction, {}>; run: (interaction: MessageContextMenuCommandInteraction) => CommandResultType; },@@ -423,7 +438,6 @@
let activeError = err; let handled = false; - // 1. Try local error handler first try { if (extra?.localOnError) { this.emit("debug", `executing local error handler for interaction ${interaction.id}`);@@ -435,7 +449,6 @@ this.emit("debug", `local error handler threw an error, propagating...`);
activeError = localErr; } - // 2. Fallback to builder-level error handlers if (!handled && extra?.builderErrorHandlers) { for (const handler of extra.builderErrorHandlers) { try {@@ -654,7 +667,9 @@ const subcommandOptions: APIApplicationCommandOption[] = Object.values(cmd.subcommands).map(
(sub) => ({ type: OPTION_TYPES.subcommand, name: sub.name, + name_localizations: sub.config.nameLocalizations, description: sub.config.description, + description_localizations: sub.config.descriptionLocalizations, options: this.transformOptionsForDiscord(sub.config.options), }), );@@ -663,11 +678,15 @@ const groupOptions: APIApplicationCommandOption[] = Object.values(cmd.groups).map(
(group) => ({ type: OPTION_TYPES.subcommandGroup, name: group.name, + name_localizations: group.config?.nameLocalizations, description: group.description, + description_localizations: group.config?.descriptionLocalizations, options: Object.values(group.subcommands).map((sub) => ({ type: OPTION_TYPES.subcommand, name: sub.name, + name_localizations: sub.config.nameLocalizations, description: sub.config.description, + description_localizations: sub.config.descriptionLocalizations, options: this.transformOptionsForDiscord(sub.config.options), })), }),@@ -680,8 +699,19 @@ description: cmd.description,
options: [...subcommandOptions, ...groupOptions], }; + if (cmd.config?.nameLocalizations) + payload.name_localizations = cmd.config.nameLocalizations; + if (cmd.config?.descriptionLocalizations) + payload.description_localizations = cmd.config.descriptionLocalizations; if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config?.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); } else {@@ -692,8 +722,18 @@ description: cmd.config.description,
options: this.transformOptionsForDiscord(cmd.config.options), }; + if (cmd.config.nameLocalizations) payload.name_localizations = cmd.config.nameLocalizations; + if (cmd.config.descriptionLocalizations) + payload.description_localizations = cmd.config.descriptionLocalizations; if (cmd.config.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); }@@ -706,8 +746,16 @@ type: ApplicationCommandType.User,
name: cmd.name, }; + if (cmd.config?.nameLocalizations) payload.name_localizations = cmd.config.nameLocalizations; if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config?.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); }@@ -718,8 +766,16 @@ type: ApplicationCommandType.Message,
name: cmd.name, }; + if (cmd.config?.nameLocalizations) payload.name_localizations = cmd.config.nameLocalizations; if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config?.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); }@@ -759,6 +815,22 @@ };
if (opt.autocomplete) { payload.autocomplete = true; + } + + if (opt.choices) { + payload.choices = opt.choices.map((c) => ({ + name: c.name, + name_localizations: c.nameLocalizations, + value: c.value, + })); + } + + if (opt.nameLocalizations) { + payload.name_localizations = opt.nameLocalizations; + } + + if (opt.descriptionLocalizations) { + payload.description_localizations = opt.descriptionLocalizations; } return payload as APIApplicationCommandBasicOption;
@@ -5,6 +5,7 @@ type Attachment,
type Role, type GuildBasedChannel, type ApplicationCommandOptionChoiceData, + type LocalizationMap, } from "discord.js"; export const OPTION_TYPES = {@@ -22,6 +23,12 @@ attachment: 11,
} as const; type OptionType = keyof typeof OPTION_TYPES; +export interface OptionChoice<T> { + name: string; + nameLocalizations?: LocalizationMap; + value: T; +} + export interface Option<T, Req extends boolean> { type: OptionType; description?: string;@@ -30,6 +37,9 @@ autocomplete?: (
interaction: AutocompleteInteraction, value: T, ) => Promise<ApplicationCommandOptionChoiceData[]> | ApplicationCommandOptionChoiceData[]; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; __type?: T; }@@ -53,29 +63,77 @@
export interface OptionBuilder<T> { ( description: string, - config: { required: true; autocomplete?: AutocompleteFn<T> }, + config: { + required: true; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, true>; ( description: string, - config?: { required?: false; autocomplete?: AutocompleteFn<T> }, + config?: { + required?: false; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, false>; - (config: { required: true; autocomplete?: AutocompleteFn<T> }): Option<T, true>; - (config?: { required?: false; autocomplete?: AutocompleteFn<T> }): Option<T, false>; + (config: { + required: true; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }): Option<T, true>; + (config?: { + required?: false; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }): Option<T, false>; required( description: string, - config?: { autocomplete?: AutocompleteFn<T> }, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, true>; optional( description: string, - config?: { autocomplete?: AutocompleteFn<T> }, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, false>; } const createOption = <T>(type: OptionType): OptionBuilder<T> => { const fn = ( - descriptionOrConfig?: string | { required?: boolean; autocomplete?: AutocompleteFn<T> }, - config?: { required?: boolean; autocomplete?: AutocompleteFn<T> }, + descriptionOrConfig?: + | string + | { + required?: boolean; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, + config?: { + required?: boolean; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): any => { const hasDescription = typeof descriptionOrConfig === "string"; const description = hasDescription ? descriptionOrConfig : undefined;@@ -86,15 +144,46 @@ type,
description, required: actualConfig?.required ?? false, autocomplete: actualConfig?.autocomplete, + choices: actualConfig?.choices, + nameLocalizations: actualConfig?.nameLocalizations, + descriptionLocalizations: actualConfig?.descriptionLocalizations, }; }; - fn.required = (description: string, config?: { autocomplete?: AutocompleteFn<T> }) => { - return fn(description, { required: true, autocomplete: config?.autocomplete }); + fn.required = ( + description: string, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, + ) => { + return fn(description, { + required: true, + autocomplete: config?.autocomplete, + choices: config?.choices, + nameLocalizations: config?.nameLocalizations, + descriptionLocalizations: config?.descriptionLocalizations, + }); }; - fn.optional = (description: string, config?: { autocomplete?: AutocompleteFn<T> }) => { - return fn(description, { required: false, autocomplete: config?.autocomplete }); + fn.optional = ( + description: string, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, + ) => { + return fn(description, { + required: false, + autocomplete: config?.autocomplete, + choices: config?.choices, + nameLocalizations: config?.nameLocalizations, + descriptionLocalizations: config?.descriptionLocalizations, + }); }; return fn as OptionBuilder<T>;