A small, simple, fully type-safe TypeScript library for defining and handling Discord slash commands and events, batteries included.
Inspired by discordx
Disenchantment is a tiny, opinionated library that layers a functional, data-driven DSL on top of discord.js v14. It lets you declare your bot’s slash commands, subcommands, options, guards, and events purely with TypeScript objects—no decorators, no “magic,” no ceremony. Everything is composable, fully typed, and ready to register with a single call.
- Object-based Slash Commands
Define commands & nested subcommands with plain TypeScript objects. - Type-Safe Options
Leverage built-in helpers to declare option types, descriptions, defaults, and validations. - Middleware-Style Guards
Attach guard functions to commands for permissions, cooldowns, rate limits, or custom logic. - Concise Event Maps
Wire up any Discord.js eventready
,messageCreate
,guildMemberAdd
, etc. in one place. - Auto-Registration
Serialize and deploy your slash commands to the Discord API with a single async call. - One-Call Bootstrap
Spin up your entire bot-client, commands, events, registration-in onecreateBot({ … })
invocation.
-
⚙️ Autocomplete
We plan to add first-class support for Discord’s autocomplete right now you’ll need to handle it yourself. -
🔄 Partial Command Updates
Smartly patch only changed commands instead of full re-deploys.
# npm
npm install disenchantment discord.js
# yarn
yarn add disenchantment discord.js
# pnpm
pnpm add disenchantment discord.js
Discord.js v14 is a peer dependency.
import {
GatewayIntentBits,
ApplicationCommandOptionType,
type CommandInteraction,
InteractionContextType,
} from "discord.js";
import {
createBot,
createCommand,
createEvent,
group,
option,
guards,
handleCommandInteraction,
initApplicationCommands,
type GuardFn,
} from "disenchantment";
// Define a simple ping command with localization
const pingCommand = createCommand({
name: "ping",
description: "Replies with Pong!",
nameLocalizations: {
// Add localized names
fr: "salut",
de: "ping",
},
descriptionLocalizations: {
// Add localized descriptions
fr: "Répond avec Pong !",
de: "Antwortet mit Pong!",
},
handler: async (interaction: CommandInteraction) => {
await interaction.reply("Pong!");
},
});
// Define a command with options, including localization for options
const addCommand = createCommand({
name: "add",
description: "Add two numbers",
options: {
x: option({
name: "x",
description: "First number",
type: ApplicationCommandOptionType.Number,
required: true,
nameLocalizations: {
// Localize option name
fr: "premier-nombre",
de: "erste-zahl",
},
descriptionLocalizations: {
// Localize option description
fr: "Le premier nombre",
de: "Die erste Zahl",
},
}),
y: option({
name: "y",
description: "Second number",
type: ApplicationCommandOptionType.Number,
required: true,
nameLocalizations: {
// Localize option name
fr: "deuxieme-nombre",
de: "zweite-zahl",
},
descriptionLocalizations: {
// Localize option description
fr: "Le deuxième nombre",
de: "Die zweite Zahl",
},
}),
},
handler: async (interaction: CommandInteraction, { x, y }) => {
await interaction.reply(`Result: ${x + y}`);
},
});
// Define a guard function (e.g., for admin-only commands)
const adminOnlyGuard: GuardFn<CommandInteraction> = (
_client,
interaction,
next,
_context,
) => {
// Replace 'YOUR_ADMIN_USER_ID' with the actual admin user ID or implement proper permission checking
if (interaction.user.id === "YOUR_ADMIN_USER_ID") {
next(); // Proceed to the command handler
} else {
interaction.reply({
content: "You don't have permission to use this command.",
ephemeral: true,
});
// Do not call next() to halt the command execution
}
};
// Define an admin-only command using the guard, with localization
const secretCommand = createCommand({
name: "secret",
description: "An admin-only secret command",
nameLocalizations: {
// Add localized names
fr: "secret",
de: "geheim",
},
descriptionLocalizations: {
// Add localized descriptions
fr: "Une commande secrète réservée aux administrateurs",
de: "Ein nur für Administratoren zugänglicher Geheim-Befehl",
},
context: [InteractionContextType.Guild],
guards: guards(adminOnlyGuard), // Apply the guard
handler: async (interaction: CommandInteraction) => {
await interaction.reply("You've accessed the secret command!");
},
});
// Group related commands, including localization for the group
const mathGroup = group("math", "Mathematical operations", [addCommand], {
// Add localized names and descriptions for the group
nameLocalizations: {
fr: "maths",
de: "mathematik",
},
descriptionLocalizations: {
fr: "Opérations mathématiques",
de: "Mathematische Operationen",
},
});
// Define an event handler for interactions
const interactionCreateEvent = createEvent({
event: "interactionCreate",
handler: async (_client, interaction) => {
if (!interaction.isChatInputCommand()) return; // Only handle chat input commands
await handleCommandInteraction(interaction); // Process the command
},
});
// Define an event handler for the bot being ready
const readyEvent = createEvent({
event: "ready",
handler: async (client) => {
await client.guilds.fetch(); // Fetch guilds if needed
// Initialize application commands - replace 'YOUR_GUILD_ID' with your guild ID for testing
// For global commands, omit the guild ID array: await initApplicationCommands(client);
await initApplicationCommands(client, ["YOUR_GUILD_ID"]);
console.log(`Logged in as ${client.user?.tag}`);
},
});
// Bootstrap the bot
(async () => {
const client = await createBot({
clientOptions: {
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], // Specify required intents
},
commands: [pingCommand, mathGroup, secretCommand], // Include all your commands and groups
events: [readyEvent, interactionCreateEvent], // Include all your event handlers
});
// Login to Discord - ensure BOT_TOKEN is set in your environment variables
await client.login(process.env.BOT_TOKEN);
})();
- Fork the repo & create a feature branch.
- Write clear, focused commits—one logical change per commit.
- Open a pull request with a description of what you’ve changed and why.
- Ensure all existing tests pass and add tests for new features.
Distributed under the MIT License. See LICENSE
for details.