How to implement, test and mock Discord.js v13 slash commands with Typescript
Tags: discord, typescript, testing, mock
Published: 2022-02-22 14:11:32

## Situation

If you have a verified Discord Bot running on over 100 servers, you surely have received this email:

> Hi there!
> We noticed that you are developer with a verified bot, and we wanted to send you a quick reminder about recently announced important changes.
> On May 1, 2022 message content and related fields will become a Privileged Intent. You can read more about this here: https://dis.gd/mcfaq.
> This means for verified apps, you will need to apply to be approved for the Intent, or you will need to migrate your commands to slash commands or use other solutions (some examples are available here: https://dis.gd/mcalternatives)

This means that bots that have commands starting with `.`, `!`, `+` and so on will stop working.
That's because the bot won't be able to access user messages unless they are approved - and this approval process is really restricted.

![Message intent request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ba467orbcur89va0xa3t.png)

However, most developers won't need to request this approval because they can just migrate their message commands to slash commands, which are now supported on discord.js library:

* [Discord.js Guide: Registering slash commands](https://discordjs.guide/interactions/registering-slash-commands.html)

## Problem

While they are very easy to implement, we don't many have examples on how to add this feature for typescript projects.  

That's still okay because we just have to type some stuff here and there.  

The thing is that internal classes from discord.js library are now private or protected, so we can't mock them as we would usually do for an advanced integration testing.  

Or can we?

## This guide

Here I will show you how I have migrated message commands to slash commands on [Corvo Astral](https://github.com/markkop/corvo-astral) and how I've adapted the tests I've made for it.

{% github https://github.com/markkop/corvo-astral no-readme %}

There are probably better and cleaner ways of doing this migration, but I hope this guide gives you enough knowledge to figure out an approach that works best for you.

If you're looking for a guide on how to create a bot from scratch with slash commands instead, check out this awesome [Build a 100 Days of Code Discord Bot with TypeScript, MongoDB, and Discord.js 13](https://www.freecodecamp.org/news/build-a-100-days-of-code-discord-bot-with-typescript-mongodb-and-discord-js-13/) guide.

## Registering Slash Commands

Your first contact when searching how to migrate to slash commands was probably this official discord.js example guide:

* [Discord.js Guide: Command Handling](https://discordjs.guide/creating-your-bot/command-handling.html)

Here they suggest creating a data file for each command and batch reading them to register their name and options.

However, if you follow the guide as itself, you'll have this problem: **Property 'commands' does not exist on type 'Client<boolean>' in Typescript**

{% stackoverflow 69500556 %}

There are some workarounds that might work, but we're using typescript and we don't want to create and extend types just to attach this command list to the client instance.

I found it better to just keep this command data objects within each command file and export them separately. I would then import them all in the `commands/index.js` file to finally export them as an array.

```js
import AboutCommand, { getData as getAboutData} from './About'
import EquipCommand, { getData as getEquipData } from './Equip'
import CalcCommand, { getData as getCalcData} from './Calc'
import RecipeCommand, { getData as getRecipeData} from './Recipe'
import SubliCommand, { getData as getSubliData } from './Subli'
import AlmaCommand, { getData as getAlmaData} from './Alma'
import HelpCommand, { getData as getHelpData} from './Help'
import ConfigCommand, { getData as getConfigData } from './Config'
import PartyCreateCommand, { getData as getPartyCreateData } from './party/PartyCreate'
import PartyUpdateCommand, { getData as getPartyUpdateData } from './party/PartyUpdate'
import PartyReaction from './party/PartyReaction'

export {
  AboutCommand,
  EquipCommand,
  CalcCommand,
  RecipeCommand,
  SubliCommand,
  AlmaCommand,
  HelpCommand,
  ConfigCommand,
  PartyCreateCommand,
  PartyUpdateCommand,
  PartyReaction
}

export default [
  getAboutData,
  getCalcData,
  getAlmaData,
  getConfigData,
  getPartyCreateData,
  getPartyUpdateData,
  getEquipData,
  getRecipeData,
  getSubliData,
  getHelpData
]
```

Note: I'm using a `getData` function instead of a `data` object because I change its content according to a language parameter. You probably can keep them as objects.

This array would be looped inside a `registerCommands` function, parsed to JSON and making a put request to the `applicationGuildCommands` discord route.

```js
export async function registerCommands (
  client: Client, 
  guildId: string, 
  guildConfig: GuildConfig, 
  guildName: string) {
  try {
    const rest = new REST({ version: "9" }).setToken(process.env.DISCORD_BOT_TOKEN);
  
    const commandData = commandsData.map((getData) => {
      const data = getData(guildConfig.lang)
      return data.toJSON()
    });
  
    await rest.put(
      Routes.applicationGuildCommands(client.user?.id || "missing id", guildId),
      { body: commandData }
    );
  
    console.log("Slash commands registered!");
  } catch (error) {
    if (error.rawError?.code === 50001) {
      console.log(`Missing Access on server "${guildName}"`)
      return
    }
    console.log(error)
  } 
};
```

Again: your `commandsData.map` callback would be as simple as  `data => data.toJSON()` if you're only using data objects.

Remember that you can check this bot's [source code](https://github.com/markkop/corvo-astral) directly to understand it better.


## Reusing slash command options

Since we're here, it might be worth to mention that you can - and should - reuse commands options.
What I mean is that you can refactor this:

```js
export const aboutCommandData = new SlashCommandBuilder()
  .setName('about')
  .setDescription('The about command')
  .addStringOption(option => option
    .setName('language')
    .setDescription('The language option'))

export const helpCommandData = new SlashCommandBuilder()
  .setName('help')
  .setDescription('The help command')
  .addStringOption(option => option
    .setName('language')
    .setDescription('The language option'))
```

To this:

```js
export function addLangStringOption(builder: SlashCommandBuilder|SlashCommandSubcommandBuilder) {
  return builder
      .addStringOption(option => option
        .setName('language')
        .setDescription('The language option'))
}

export const aboutCommandData = (lang: string) => {
  const builder = new SlashCommandBuilder()
  builder
    .setName('about')
    .setDescription('The about command')
  addLangStringOption(builder)
  return builder
}

export const helpCommandData = (lang: string) => {
  const builder = new SlashCommandBuilder()
  builder
    .setName('help')
    .setDescription('The help command')
  addLangStringOption(builder)
  return builder
}
```
You can extend this approach even further with subcommands and choices, like this:

```js
export function addStringOptionWithLanguageChoices(
  builder: SlashCommandBuilder|SlashCommandSubcommandBuilder, 
  name: string, 
  description: string
) {
  return builder.addStringOption(option => {
    option
      .setName(name)
      .setDescription(description)
      ['pt', 'en', 'fr', 'es'].forEach(language =>
        option.addChoice(language, language)
      )
      return option
    })
}

export function addLangAndTranslateStringOptions(
  builder: SlashCommandBuilder|SlashCommandSubcommandBuilder,
  lang: string
) {
  addLangStringOption(builder, lang)
  addStringOptionWithLanguageChoices(
    builder, 
    'translate',
    stringsLang.translateCommandOptionDescription[lang]
  )
  return builder
}

export function addLangStringOption(
  builder: SlashCommandBuilder|SlashCommandSubcommandBuilder,
  lang: string
) {
  return addStringOptionWithLanguageChoices(
    builder, 
    'lang', 
    stringsLang.langCommandOptionDescription[lang]
  )
}
```
(check the source code [here](https://github.com/Markkop/corvo-astral/blob/af0b4617d801479f59ae9f19bd944e4513fa7fec/src/utils/registerCommands.ts))

## Mocking Discord.js

When I first started this project a few years ago, I was not using Typescript and mocking user messages was simple as this

```js
import config from '../src/config'
const { defaultConfig: { partyChannel } } = config

/**
 * Mocks a channel message to match properties from a Discord Message.
 * Note that channel messages has actually Collection type and here we're treating them
 * as arrays and enriching their properties to have the same as a Discord Collection.
 *
 * @param {string} content
 * @param {object[]} channelMessages
 * @returns {object}
 */
export function mockMessage (content, channelMessages = []) {
  channelMessages.forEach(channelMessages => { channelMessages.edit = jest.fn(message => message) })
  channelMessages.nativeFilter = channelMessages.filter
  channelMessages.filter = (func) => {
    const filtered = channelMessages.nativeFilter(func)
    filtered.first = () => filtered[0]
    filtered.size = filtered.length
    return filtered
  }
  return {
    react: jest.fn(),
    content: content,
    channel: {
      send: jest.fn(message => {
        if (typeof message === 'object') {
          message.react = jest.fn()
        }
        return message
      })
    },
    author: {
      id: 111,
      username: 'Mark'
    },
    guild: {
      id: 100,
      name: 'GuildName',
      channels: {
        cache: [
          {
            name: partyChannel,
            messages: {
              fetch: jest.fn().mockResolvedValue(channelMessages)
            },
            send: jest.fn(message => {
              message.react = jest.fn()
              return message
            })
          }
        ]
      }
    }
  }
}
```

Ugly, right?  

Well, it was working until I migrated the bot to typescript and a more Object-Oriented Programming structure and got showered with typing errors.

This made me to understand how Discord.js worked and it let me create an entire mocking structure (you'll see it in the next code snippet).

But now with v13, Discord changed their internal classes to be private or protected, so we can't just call `new` when instancing one of them.

That's what I thought.

{% github https://github.com/discordjs/discord.js/issues/6798 %} 

Thanks to this issue, I not only found out we could simply use `@ts-expect-error` comment lines but we could also use  [Reflect.construct](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct) javascript method.

This is really awesome because we don't have to change our mocking structure anymore.

Here's a lean example of this mocking strucure with the `Reflect.construct` solution.

```js
import {
  Client,
  User,
  CommandInteraction,
} from "discord.js";

export default class MockDiscord {
  private client!: Client;
  private user!: User;
  public interaction!: CommandInteraction;
  
  constructor(options) {
    this.mockClient();
    this.mockUser();
    this.mockInteracion(options?.command)
  }

  public getInteraction(): CommandInteraction {
    return this.interaction;
  }

  private mockClient(): void {
    this.client = new Client({ intents: [], restSweepInterval: 0 });
    this.client.login = jest.fn(() => Promise.resolve("LOGIN_TOKEN"));
  }

  private mockUser(): void {
    this.user = Reflect.construct(User, [
        this.client, {
          id: "user-id",
          username: "USERNAME",
          discriminator: "user#0000",
          avatar: "user avatar url",
          bot: false,
        }
      ]
    )
  }

  private mockInteracion(command): void {
    this.interaction = Reflect.construct(CommandInteraction, [
      this.client,
        {
          data: command,
          id: BigInt(1),
          user: this.user,
        }
      ]
    )
    this.interaction.reply = jest.fn()
    this.interaction.isCommand = jest.fn(() => true)
  }
}
```

For a more accurate mock, please refer to my [mockDiscord.ts](https://github.com/markkop/corvo-astral/blob/master/tests/mockDiscord.ts) file on the project's repository.

## Testing commands

Now that we have our discord.js mock setup, we just need some testing function helpers and the test files themselves.

Our main testing function will be `executeCommandAndSpyReply`:

```js
import MockDiscord from './mockDiscord'

/* Spy 'reply' */
export function mockInteractionAndSpyReply(command) {
  const discord = new MockDiscord({ command })
  const interaction = discord.getInteraction() as CommandInteraction
  const spy = jest.spyOn(interaction, 'reply') 
  return { interaction, spy }
}

export async function executeCommandAndSpyReply(command, content, config = {}) {
  const { interaction, spy } = mockInteractionAndSpyReply(content)
  const commandInstance = new command(interaction, {...defaultConfig, ...config})
  await commandInstance.execute()
  return spy
}
```

Since I'm using an OOP approach for this bot, I have to call `new` command to be able to `execute()` it, but I'm sure you'll be able to adapt to however you've built your bot.

As you can see, we first initialize a DiscordJS instance, passing a mocked command parameter that will be then used to mock an interaction as well.

That `command` argument is an object that contains the data of a [CommandInteraction](https://github.com/discordjs/discord.js/blob/b936103395121cb21a8c616f669ddab1d2efb0f1/packages/discord.js/src/structures/CommandInteraction.js#L136).

We could build it ourselves, but I found easier to write a parse function to read a single string command, such as `/config set lang: en` and transform it to the expected object.

Here is the result

```js
// Usage
it('returns the matching sublimation when using query without accents', async () => {
  const stringCommand = '/subli by-name name: influencia lang: pt'
  const command = getParsedCommand(stringCommand, commandData)
  const spy = await executeCommandAndSpyReply(SubliCommand, command)
  expect(spy).toHaveBeenCalledWith(embedContaining({
    title: ':scroll: Influência I'
  }))
})
```

```js
// getParsedCommand implementation 

export const optionType = {
  // 0: null,
  // 1: subCommand,
  // 2: subCommandGroup,
  3: String,
  4: Number,
  5: Boolean,
  // 6: user,
  // 7: channel,
  // 8: role,
  // 9: mentionable,
  10: Number,
}

function getNestedOptions(options) {
  return options.reduce((allOptions, option) => {
    if (!option.options) return [...allOptions, option]
    const nestedOptions = getNestedOptions(option.options)
    return [option, ...allOptions, ...nestedOptions]
  }, [])
}

function castToType(value: string, typeId: number) {
  const typeCaster = optionType[typeId]
  return typeCaster ? typeCaster(value) : value
}

export function getParsedCommand(stringCommand: string, commandData) {
  const options = getNestedOptions(commandData.options)
  const optionsIndentifiers = options.map(option => `${option.name}:`)
  const requestedOptions = options.reduce((requestedOptions, option) => {
    const identifier = `${option.name}:`
    if (!stringCommand.includes(identifier)) return requestedOptions
    const remainder = stringCommand.split(identifier)[1]

    const nextOptionIdentifier = remainder.split(' ').find(word => optionsIndentifiers.includes(word))
    if (nextOptionIdentifier) {
      const value = remainder.split(nextOptionIdentifier)[0].trim()
      return [...requestedOptions, {
        name: option.name,
        value: castToType(value, option.type),
        type: option.type
      }]
    }
    
    return [...requestedOptions, {
      name: option.name,
      value: castToType(remainder.trim(), option.type),
      type: option.type
    }]
  }, [])
  const optionNames = options.map(option => option.name)
  const splittedCommand = stringCommand.split(' ')
  const name = splittedCommand[0].replace('/', '')
  const subcommand = splittedCommand.find(word => optionNames.includes(word))
  return {
    id: name,
    name,
    type: 1,
    options: subcommand ? [{
      name: subcommand,
      type: 1,
      options: requestedOptions
    }] : requestedOptions
  }
}
```

I've tried several ways to get parse a command and this is the best I've found. It uses the command data that was generated by `SlashCommandBuilder` (in my case it's a function because I change its language dynamically) to identify which subcommands and options are being used.  

Please note that in its current way it only identifies one subcommand, so a refactor would be needed to support multiple subcommands.

If you find this implementation too complex, feel free to pass a `command` object using its data structure directly:

```json
{
  "id": "config",
  "name": "config",
  "type": 1,
  "options": [
    {
      "type": 1,
      "name": "set",
      "options": [{
        "value": "en",
        "type": 3,
        "name": "lang"
      }],
    }
  ]
}
```

## Conclusion

Due to the complexity of my own bot, the content I've just presented might be too much for your needs.  
But I'm sure that my code examples will help you in some way.

Working on the migration from message commands to slash commands have been an awesome challenge. There's not much on the internet currently about it, so I had to rely mostly on the Discord.js source code itself.

If you're having any trouble with it, feel free to comment here, open an issue on the Corvo Astral repository (even if it's about your own project) or contact me by twitter!

## References

* [Build a 100 Days of Code Discord Bot with TypeScript, MongoDB, and Discord.js 13](https://www.freecodecamp.org/news/build-a-100-days-of-code-discord-bot-with-typescript-mongodb-and-discord-js-13/)
* [Discord.js Source Code](https://github.com/discordjs/discord.js/)
* [Discord.js Documentation](https://discordjs.guide/creating-your-bot/command-handling.html#individual-command-files)