Skip to content

Commit

Permalink
fix: introduce SubclassConstructor, CommandSubclass, SubCommandSubcla…
Browse files Browse the repository at this point in the history
…ss and SubcommandableCommandSubclass typings

This fixes the types on CommandManager to be correctly assigned as subclasses of AbstractCommand, not instances of it
  • Loading branch information
Amgelo563 committed Jun 2, 2022
1 parent 888b288 commit feca37f
Show file tree
Hide file tree
Showing 13 changed files with 73 additions and 51 deletions.
30 changes: 17 additions & 13 deletions src/CommandManager.ts
@@ -1,28 +1,32 @@
import { ApplicationCommandDataResolvable, Collection } from 'discord.js';
import fs from 'fs';

import Command from './structures/commands/Command';
import BotType from './Bot';
import ApplicationCommand from './structures/commands/ApplicationCommand';
import { getApplicationCommands } from './utils/CommandUtils';
import Bot from './Bot';
import AbstractCommand from './structures/commands/AbstractCommand';
import { SubclassConstructor } from './structures/types';
import { CommandSubclass } from './structures/commands/Command';

export type AbstractCommandSub = SubclassConstructor<typeof AbstractCommand>;

export default class CommandManager {
/** The bot that instanciated this manager */
private readonly bot: BotType;
private readonly bot: Bot;

/** Array of all the available command classes */
private readonly commandClasses: typeof Command[] = [];
private readonly commandClasses: AbstractCommandSub[] = [];

/** Collection of <Category, Commands from that category> */
private readonly categories: Collection<string, typeof Command[]> = new Collection();
private readonly categories: Collection<string, AbstractCommandSub[]> = new Collection();

/** Array of all the available main command names */
private readonly mainCommands: string[] = [];

/** Collection of <Command name|alias, Command class> */
private readonly commands: Collection<string, typeof Command> = new Collection<string, typeof Command>();
private readonly commands: Collection<string, AbstractCommandSub> = new Collection();

constructor(bot: BotType) {
constructor(bot: Bot) {
this.bot = bot;
}

Expand All @@ -44,7 +48,7 @@ export default class CommandManager {
try {
// ? Eslint is right about this one, but I haven't found any other good way to achieve this
// eslint-disable-next-line global-require,import/no-dynamic-require
const command: typeof Command = require(path).default;
const command: AbstractCommandSub = require(path).default;
category.push(command);
this.mainCommands.push(command.data.names[0]);
for (const name of command.data.names) {
Expand All @@ -67,7 +71,7 @@ export default class CommandManager {

/** Loads ApplicationCommands. */
private async loadApplicationCommands(): Promise<void> {
const commands: Collection<string, ApplicationCommand> = getApplicationCommands(this.getCommandClasses());
const commands: Collection<string, ApplicationCommand> = getApplicationCommands(this.getCommandClasses() as unknown as CommandSubclass[]);

const commandManager = this.bot.client.application?.commands;
if (!commandManager) return;
Expand All @@ -77,22 +81,22 @@ export default class CommandManager {
}

/** Get the array of all the available command classes */
getCommandClasses() {
getCommandClasses(): AbstractCommandSub[] {
return this.commandClasses;
}

/** Get the collection of <Category, Commands from that category> */
getCategories(): Collection<string, typeof Command[]> {
getCategories(): Collection<string, AbstractCommandSub[]> {
return this.categories;
}

/** Get the collection of <Command name|alias, Command class> */
getCommands(): Collection<string, typeof Command> {
getCommands(): Collection<string, AbstractCommandSub> {
return this.commands;
}

/** Get the array of all the available main command names */
getMainCommands() {
getMainCommands(): string[] {
return this.mainCommands;
}
}
7 changes: 4 additions & 3 deletions src/MessageArgumentsParser.ts
Expand Up @@ -25,6 +25,7 @@ import {
} from './utils/DiscordUtils';
import SubcommandableCommand from './structures/commands/SubcommandableCommand';
import SubCommand from './structures/commands/SubCommand';
import { SubclassConstructor } from './structures/types';
import Command from './structures/commands/Command';

// ? An async Array#every (https://advancedweb.hu/how-to-use-async-functions-with-array-some-and-every-in-javascript/)
Expand All @@ -50,12 +51,12 @@ export default class MessageArgumentsParser {
private readonly original: ApplicationCommandOptionData[];

/** The name of the command */
private readonly command: typeof Command | typeof SubcommandableCommand;
private readonly command: SubclassConstructor<typeof Command>;

constructor(
message: Message<true>,
args: string[],
command: typeof Command | typeof SubcommandableCommand,
command: SubclassConstructor<typeof Command>,
original: ApplicationCommandOptionData[],
) {
this.message = message;
Expand Down Expand Up @@ -171,7 +172,7 @@ export default class MessageArgumentsParser {
}

case 'SUB_COMMAND': {
const SubcommandClass: typeof SubCommand | undefined = (this.command as typeof SubcommandableCommand)
const SubcommandClass: typeof SubCommand | undefined = (this.command as unknown as typeof SubcommandableCommand)
.getSubCommands()
.find((subcommand) => subcommand.data.names.includes(input));
if (!SubcommandClass) return false;
Expand Down
16 changes: 8 additions & 8 deletions src/commands/examplecategory/HelpCommand.ts
Expand Up @@ -4,10 +4,10 @@ import {
ApplicationCommandOptionData,
} from 'discord.js';

import Command from '../../structures/commands/Command';
import Command, { CommandSubclass } from '../../structures/commands/Command';
import { toTitleCase } from '../../utils/StringUtils';

import SubcommandableCommand from '../../structures/commands/SubcommandableCommand';
import SubcommandableCommand, { SubcommandableCommandSubclass } from '../../structures/commands/SubcommandableCommand';
import { CommandData } from '../../structures/types';
import Bot from '../../Bot';

Expand Down Expand Up @@ -53,7 +53,7 @@ export default class HelpCommand extends Command {
let description = '';

category.forEach((categoryCommand) => {
description += `**${this.prefix}${categoryCommand.getOverview()}\n`;
description += `**${this.prefix}${(categoryCommand as unknown as CommandSubclass).getOverview()}\n`;
});

return this.reply(new MessageEmbed()
Expand All @@ -67,13 +67,13 @@ export default class HelpCommand extends Command {

if (!subCommandName || !(FoundCommand instanceof SubcommandableCommand)) return this.reply(await FoundCommand.getUsage(this.prefix));

// FIXME The type is somehow wrong here, CommandClass appears as AbstractCommand, when it's actually a subclass of it.
// @ts-ignore See above
const commandInstance = new FoundCommand(this.bot, commandName, this.source, this.prefix);
const SubcommandableCommandClass = FoundCommand as unknown as SubcommandableCommandSubclass;

const commandInstance = new SubcommandableCommandClass(this.bot, commandName, this.source, this.prefix, this.options);
const FoundSubCommand = commandInstance.getSubCommand(subCommandName);

if (!FoundSubCommand) return this.reply(await FoundCommand.getUsage(this.prefix));
return this.reply(await FoundSubCommand.constructor.getUsage(this.prefix));
return this.reply(await FoundSubCommand.getConstructor().getUsage(this.prefix));
}

return this.sendUsage();
Expand Down Expand Up @@ -108,7 +108,7 @@ export default class HelpCommand extends Command {

const focusedCommand = interaction.options.getString('command');
if (!focusedCommand) return options;
const command = bot.commands.getCommands().get(focusedCommand) as typeof SubcommandableCommand;
const command = bot.commands.getCommands().get(focusedCommand) as unknown as SubcommandableCommandSubclass;
if (!command || !command.getSubCommands) return options;
return command.getSubCommands().map((subcommand) => subcommand.data.names[0]);
}
Expand Down
6 changes: 4 additions & 2 deletions src/events/interactionCreate/AutocompleteResponder.ts
@@ -1,19 +1,21 @@
import { AutocompleteInteraction } from 'discord.js';
import SubcommandableCommand from '../../structures/commands/SubcommandableCommand';
import Event from '../../structures/Event';
import { SubclassConstructor } from '../../structures/types';

export default class AutocompleteResponder extends Event {
override async run(interaction: AutocompleteInteraction) {
if (!interaction.isAutocomplete()) return;

const commandInput = [interaction.commandName, interaction.options.getSubcommand(false)];

const mainCommand = this.bot.commands.getCommands().get(commandInput.shift() as string) as typeof SubcommandableCommand;
const mainCommand = this.bot.commands.getCommands()
.get(commandInput.shift() as string) as unknown as SubclassConstructor<typeof SubcommandableCommand>;
let command = mainCommand;
if (commandInput.shift()) {
command = mainCommand
.getSubCommands()
.find((subcommand) => subcommand.data.names[0] === commandInput[0]) as unknown as typeof SubcommandableCommand;
.find((subcommand) => subcommand.data.names[0] === commandInput[0]) as unknown as SubclassConstructor<typeof SubcommandableCommand>;
}

const focusedOption = interaction.options.getFocused(true);
Expand Down
15 changes: 9 additions & 6 deletions src/events/interactionCreate/SlashCommandsExecutor.ts
@@ -1,17 +1,22 @@
import { GuildMember, Interaction, MessageEmbed } from 'discord.js';
import {
CommandInteractionOptionResolver,
GuildMember,
Interaction,
MessageEmbed,
} from 'discord.js';

import CommandSource from '../../structures/commands/CommandSource';
import { Symbols, Colors } from '../../utils/Constants';
import { sendTemporal, canMemberExecute } from '../../utils/DiscordUtils';
import Command from '../../structures/commands/Command';
import { CommandSubclass } from '../../structures/commands/Command';
import Event from '../../structures/Event';

export default class SlashCommandsExecutor extends Event {
override async run(interaction: Interaction): Promise<void> {
if (!interaction.isCommand() && !interaction.isContextMenu()) return;

const name = interaction.commandName;
const CommandClass = this.bot.commands.getCommands().get(name) as typeof Command;
const CommandClass = this.bot.commands.getCommands().get(name) as unknown as CommandSubclass;

if (CommandClass === undefined) return;

Expand All @@ -33,9 +38,7 @@ ${missingPerms.missingPerms}
}
const source = new CommandSource(interaction);

// FIXME The type is somehow wrong here, CommandClass appears as AbstractCommand, when it's actually a subclass of it.
// @ts-ignore See above
const cmd = new CommandClass(this.bot, name, source, '/', interaction.options);
const cmd = new CommandClass(this.bot, name, source, '/', interaction.options as CommandInteractionOptionResolver);
await cmd.execute();
}
}
6 changes: 3 additions & 3 deletions src/events/messageCreate/CommandsExecutor.ts
Expand Up @@ -7,6 +7,8 @@ import { getCommandName } from '../../utils/CommandUtils';
import Event from '../../structures/Event';
import MessageArgumentsParser from '../../MessageArgumentsParser';
import { split } from '../../utils/StringUtils';
import Command from '../../structures/commands/Command';
import { SubclassConstructor } from '../../structures/types';

export default class CommandsExecutor extends Event {
override async run(message: Message) {
Expand All @@ -15,7 +17,7 @@ export default class CommandsExecutor extends Event {
const commands = this.bot.commands.getCommands();
if (!parsedMessage.isCommand) return;

const CommandClass = commands.get(parsedMessage.name as string);
const CommandClass = commands.get(parsedMessage.name as string) as unknown as SubclassConstructor<typeof Command>;
if (!CommandClass) return;

const missingPerms = canMemberExecute(message.member, CommandClass.data);
Expand Down Expand Up @@ -52,8 +54,6 @@ ${missingPerms.missingPerms}
return;
}

// FIXME The type is somehow wrong here, CommandClass appears as AbstractCommand, when it's actually a subclass of it.
// @ts-ignore See above
const cmd = new CommandClass(
this.bot,
parsedMessage.name,
Expand Down
7 changes: 5 additions & 2 deletions src/structures/commands/AbstractCommand.ts
Expand Up @@ -15,11 +15,14 @@ import {
BaseOptions,
BaseAdditions,
CommandData,
SubclassConstructor,
} from '../types';
import CommandSource from './CommandSource';
import { Colors, Symbols } from '../../utils/Constants';
import { getMessageOptions, sendTemporal } from '../../utils/DiscordUtils';

export type AbstractCommandSubclass = SubclassConstructor<typeof AbstractCommand>;

/* A Command, Subcommand group or SubCommand */
export default abstract class AbstractCommand {
/** The bot's last answer to this command */
Expand All @@ -37,7 +40,7 @@ export default abstract class AbstractCommand {
/** Prefix used to call the command or main command */
protected prefix: string;

protected constructor(
constructor(
bot: Bot,
source: CommandSource,
prefix: string,
Expand Down Expand Up @@ -65,7 +68,7 @@ export default abstract class AbstractCommand {
public abstract execute(): Promise<unknown>;

/** This lets access static members from a non static view (https://github.com/Microsoft/TypeScript/issues/3841) */
protected getConstructor() {
public getConstructor() {
return Object.getPrototypeOf(this).constructor;
}

Expand Down
4 changes: 2 additions & 2 deletions src/structures/commands/ApplicationCommand.ts
@@ -1,5 +1,5 @@
import { ApplicationCommandOptionData, ApplicationCommandType } from 'discord.js';
import Command from './Command';
import { CommandSubclass } from './Command';

/* An Application Command ready to be processed and registered for Discord */
export default class ApplicationCommand {
Expand All @@ -15,7 +15,7 @@ export default class ApplicationCommand {
/* The options used to register this Application Command (only for Slash Commands) */
public options: ApplicationCommandOptionData[] | undefined;

constructor(command: typeof Command, type: ApplicationCommandType) {
constructor(command: CommandSubclass, type: ApplicationCommandType) {
this.type = type;
[this.name] = command.data.names;
if (type === 'CHAT_INPUT') {
Expand Down
5 changes: 3 additions & 2 deletions src/structures/commands/Command.ts
Expand Up @@ -4,10 +4,11 @@ import {
} from 'discord.js';

import AbstractCommand from './AbstractCommand';

import CommandSource from './CommandSource';

import Bot from '../../Bot';
import { SubclassConstructor } from '../types';

export type CommandSubclass = SubclassConstructor<typeof Command>;

/* A fully executable Command by the CommandManager */
export default abstract class Command extends AbstractCommand {
Expand Down
7 changes: 5 additions & 2 deletions src/structures/commands/SubCommand.ts
@@ -1,9 +1,12 @@
import { MessageEmbed } from 'discord.js';

import AbstractCommand from './AbstractCommand';
import Command from './Command';
import Command, { CommandSubclass } from './Command';
import Bot from '../../Bot';
import CommandSource from './CommandSource';
import { SubclassConstructor } from '../types';

export type SubCommandSubclass = SubclassConstructor<typeof SubCommand>;

/** An abstract SubCommand belonging to a Command parent. */
export default abstract class SubCommand extends AbstractCommand {
Expand All @@ -13,7 +16,7 @@ export default abstract class SubCommand extends AbstractCommand {
}

/** This subcommand's parent */
public static parentCommand: typeof Command;
public static parentCommand: CommandSubclass;

/** Get an embed about this subcommand's usage */
public static override getUsage(prefix: string): MessageEmbed {
Expand Down
13 changes: 7 additions & 6 deletions src/structures/commands/SubcommandableCommand.ts
Expand Up @@ -3,24 +3,27 @@ import {
MessageEmbed,
} from 'discord.js';

import Command from './Command';
import SubCommand from './SubCommand';
import Command, { CommandSubclass } from './Command';
import SubCommand, { SubCommandSubclass } from './SubCommand';
import { SubclassConstructor } from '../types';

export type SubcommandableCommandSubclass = SubclassConstructor<typeof SubcommandableCommand>;

/** A template class representing a Subcommandable Command (Command which contains subcommands) */
export default abstract class SubcommandableCommand extends Command {
/** The executed subcommand */
subCommand!: SubCommand;

/** List of Subcommand Classes belonging to this Command */
static getSubCommands(): Array<typeof SubCommand> {
static getSubCommands(): SubCommandSubclass[] {
return [];
}

static override getOptions(): ApplicationCommandOptionData[] {
return this.getSubCommands().map((c) => {
// ? We're mapping all the subcommands, so it's a valid usage here
// eslint-disable-next-line no-param-reassign
c.parentCommand = this as typeof Command;
c.parentCommand = this as unknown as CommandSubclass;
return {
type: 'SUB_COMMAND',
name: c.data.names[0],
Expand Down Expand Up @@ -56,8 +59,6 @@ export default abstract class SubcommandableCommand extends Command {

for (const SC of (this.getConstructor() as unknown as typeof SubcommandableCommand).getSubCommands()) {
if (SC.data.names.includes(name)) {
// FIXME The type is somehow wrong here, SC appears as SubCommand, when it's actually a subclass of it.
// @ts-ignore See above
this.subCommand = new SC(this.bot, this.source, this.prefix, this.getConstructor());
return this.subCommand;
}
Expand Down
4 changes: 4 additions & 0 deletions src/structures/types.ts
Expand Up @@ -79,3 +79,7 @@ export type BaseOptions =

/** Additions that can be added to BaseOptions */
export type BaseAdditions = MessageEmbed | MessageAttachment | MessageActionRow;

/** Type of constructor of a subclass of an abstract class */
export type SubclassConstructor<TCtor extends abstract new (...args: any[]) => any> =
Pick<TCtor, keyof TCtor> & (new (...args: ConstructorParameters<TCtor>) => InstanceType<TCtor>);

0 comments on commit feca37f

Please sign in to comment.