diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js
index 60b7719a0ff0..d1e4de84247a 100644
--- a/src/structures/ApplicationCommand.js
+++ b/src/structures/ApplicationCommand.js
@@ -156,6 +156,111 @@ class ApplicationCommand extends Base {
return this.manager.delete(this, this.guildId);
}
+ /**
+ * Whether this command equals another command. It compares all properties, so for most operations
+ * it is advisable to just compare `command.id === command2.id` as it is much faster and is often
+ * what most users need.
+ * @param {ApplicationCommand|ApplicationCommandData|APIApplicationCommand} command The command to compare with
+ * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same
+ * order in the array The client may not always respect this ordering!
+ * @returns {boolean}
+ */
+ equals(command, enforceOptionOrder = false) {
+ // If given an id, check if the id matches
+ if (command.id && this.id !== command.id) return false;
+
+ // Check top level parameters
+ const commandType = typeof command.type === 'string' ? command.type : ApplicationCommandTypes[command.type];
+ if (
+ command.name !== this.name ||
+ ('description' in command && command.description !== this.description) ||
+ (commandType && commandType !== this.type) ||
+ // Future proof for options being nullable
+ // TODO: remove ?? 0 on each when nullable
+ (command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
+ (command.defaultPermission ?? command.default_permission ?? true) !== this.defaultPermission
+ ) {
+ return false;
+ }
+
+ if (command.options) {
+ return this.constructor.optionsEqual(this.options, command.options, enforceOptionOrder);
+ }
+ return true;
+ }
+
+ /**
+ * Recursively checks that all options for an {@link ApplicationCommand} are equal to the provided options.
+ * In most cases it is better to compare using {@link ApplicationCommand#equals}
+ * @param {ApplicationCommandOptionData[]} existing The options on the existing command,
+ * should be {@link ApplicationCommand#options}
+ * @param {ApplicationCommandOptionData[]|APIApplicationCommandOption[]} options The options to compare against
+ * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same
+ * order in the array The client may not always respect this ordering!
+ * @returns {boolean}
+ */
+ static optionsEqual(existing, options, enforceOptionOrder = false) {
+ if (existing.length !== options.length) return false;
+ if (enforceOptionOrder) {
+ return existing.every((option, index) => this._optionEquals(option, options[index], enforceOptionOrder));
+ }
+ const newOptions = new Map(options.map(option => [option.name, option]));
+ for (const option of existing) {
+ const foundOption = newOptions.get(option.name);
+ if (!foundOption || !this._optionEquals(option, foundOption)) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks that an option for an {@link ApplicationCommand} is equal to the provided option
+ * In most cases it is better to compare using {@link ApplicationCommand#equals}
+ * @param {ApplicationCommandOptionData} existing The option on the existing command,
+ * should be from {@link ApplicationCommand#options}
+ * @param {ApplicationCommandOptionData|APIApplicationCommandOption} option The option to compare against
+ * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options or choices are in the same
+ * order in their array The client may not always respect this ordering!
+ * @returns {boolean}
+ * @private
+ */
+ static _optionEquals(existing, option, enforceOptionOrder = false) {
+ const optionType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type];
+ if (
+ option.name !== existing.name ||
+ optionType !== existing.type ||
+ option.description !== existing.description ||
+ (option.required ?? (['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(optionType) ? undefined : false)) !==
+ existing.required ||
+ option.choices?.length !== existing.choices?.length ||
+ option.options?.length !== existing.options?.length
+ ) {
+ return false;
+ }
+
+ if (existing.choices) {
+ if (
+ enforceOptionOrder &&
+ !existing.choices.every(
+ (choice, index) => choice.name === option.choices[index].name && choice.value === option.choices[index].value,
+ )
+ ) {
+ return false;
+ }
+ if (!enforceOptionOrder) {
+ const newChoices = new Map(option.choices.map(choice => [choice.name, choice]));
+ for (const choice of existing.choices) {
+ const foundChoice = newChoices.get(choice.name);
+ if (!foundChoice || foundChoice.value !== choice.value) return false;
+ }
+ }
+ }
+
+ if (existing.options) {
+ return this.optionsEqual(existing.options, option.options, enforceOptionOrder);
+ }
+ return true;
+ }
+
/**
* An option for an application command or subcommand.
* @typedef {Object} ApplicationCommandOption
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 70ccaeeb370e..2921a38723b8 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -220,6 +220,20 @@ export class ApplicationCommand extends Base {
public type: ApplicationCommandType;
public delete(): Promise>;
public edit(data: ApplicationCommandData): Promise>;
+ public equals(
+ command: ApplicationCommand | ApplicationCommandData | RawApplicationCommandData,
+ enforceOptionorder?: boolean,
+ ): boolean;
+ public static optionsEqual(
+ existing: ApplicationCommandOption[],
+ options: ApplicationCommandOption[] | ApplicationCommandOptionData[] | APIApplicationCommandOption[],
+ enforceOptionorder?: boolean,
+ ): boolean;
+ private static _optionEquals(
+ existing: ApplicationCommandOption,
+ options: ApplicationCommandOption | ApplicationCommandOptionData | APIApplicationCommandOption,
+ enforceOptionorder?: boolean,
+ ): boolean;
private static transformOption(option: ApplicationCommandOptionData, received?: boolean): unknown;
}