KartikeSingh / discord-bot-ts

An template for a discord bot made with typescript.
6 stars 4 forks source link

Code Quality #1

Closed Lioness100 closed 3 years ago

Lioness100 commented 3 years ago

Hello! I was looking through this repository (thanks for making it, btw) and saw a few bits of code I thought could be improved. All of this is my opinion, so I just wanted to bring it up and submit a pull request if you agree on some of the fixes I propose.

tsconfig.json

Typescript projects cannot build without a tsconfig.json. Typescript can make one for you if you doesnload typescript globally and run tsc --init, but I recommend you put in some research about it yourself.

Dev Dependencies

First of all, axios provides it's own types, so you should uninstall @types/axios. Next, typescript should be devDependencies, not dependencies because neither are used in the code itself. Furthermore, you use ts-node in the dev script, but that isn't installed, and should also be added to devDependencies . Lastly, you should create a start and especially a build script using node and tsc (typescript compiler) respectively.

{
  "name": "discord-bot-template-ts",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
+  "start": "node .",
-  "dev": "ts-node src/index.ts"
+  "dev": "ts-node src/index.ts",
+  "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
-   "@types/axios": "^0.14.0",
    "axios": "^0.21.4",
    "discord.js": "^13.1.0",
    "dotenv": "^10.0.0",
    "mongoose": "^6.0.7",
-   "typescript": "^4.4.3"
  }
+ "devDependencies": {
+   "@types/axios": "^0.14.0",
+   "ts-node": "^10.2.1",
+   "typescript": "^4.4.3"
+ }
}

Note that obviously just moving and adding lines around won't affect the dependencies- you'll need to npm uninstall ... and npm i -D ... individually

Dotenv

This is totally a personal preference, but note that instead of this:

import { config } from 'dotenv';
// ...

config();

You can just do this:

import 'dotenv/config';

If you want to go even farther, you could omit dotenv from the code altogether, and simply add -r dotenv/config before the . in the start script. If you want to do this with ts-node too, you can replace the dev script with:

"node -r ts-node/register -r dotenv/config src/index.ts"

Import Order

Another totally subjective code styling preference, but you might want to adjust the import order to look nice and pretty :). Usually this means:

Non-default imports on top, sorted by verbosity Default imports in the middle, sorted by verbosity

(This would be repeated for import type statements, which would go on top of regular statements)

import { Intents } from 'discord.js';
import { config } from 'dotenv';
import bot from './classes/bot';
import * as events from './events';

Intents

This might be a conscious readability decision on your part, but why go through the process of importing Intents, creating a new bitfield, and adding the needed intents, when you can just put an array and d.js will do the rest for you?

const client = new bot({
    // Adding the intents.
-  intents: new Intents().add("GUILDS", "GUILD_MESSAGES")
+ intents: ["GUILDS", "GUILD_MESSAGES"]
});

Typo

- // Loading the enviroment variables
+ // Loading the environment variables

Event Subscription

Optimization

Doing this:

const obj = {};

Object.keys(obj).forEach(v, i) => {
  const entry = Object.values(obj)[i];
  // ...
})

Is incredible unnecessary, verbose, and slow. Instead, just use Object.entries() which will provide you both the key and value at once (in a tuple)

- Object.keys(events).forEach((v, i) => {
-     client.on(v, (a, b, c, d) => Object.values(events)[i](client, a, b, c, d));
- })
+ Object.entries(events).forEach(([event, run]) => {
+    client.on(event, (a, b, c, d) => run(client, a, b, c, d))
+ })

Scalability

Accepting the parameters (a, b, c, d) is a recipe for disaster. It's not reliable or scalable at all. What if you have an event that has five parameters? Maybe you could add an e, but what if there were 15 parameters? 200?

Instead use a rest parameter (...args), which will account for every parameter passed to it (args will be an array). Furthermore, if you want to be a bit fancier than client.on(event, (...args) => run(client, ...args)), you can use Function#bind().

Object.entries(events).forEach(([event, run]) => {
- client.on(event, (a, b, c, d) => run(client, a, b, c, d))
+ client.on(event, run.bind(null, client))
})

Scalability Part 2

It's going to be tedious to import and export each command and listener you make. Instead, consider reading the directory dynamically with an external package such as klaw, or the builtin package, fs

- import * as events from './events'
+ import { readdirSync} from 'fs';
+ import { join } from 'path';
// ...

+ const eventsDir = join(__dirname, './events');
+ const files = readdirSync(eventsDir);
+ files.forEach((file) => {
+   import(`${eventsDir}/${file}`).then((event) => {
+     client.on(file.split('.')[0], event.bind(null, client));
+   });
+ });

For the command handler:

- import * as commands from '../commands';
+ import { readdirSync} from 'fs';
+ import { join } from 'path';
// ...

+ const commandsDir = join(__dirname, './events');
+ const categories = readdirSync(eventsDir);
+ categories.forEach((category) => {
+   const files = readdirSync(`${commandsDir}/${category}`);
+   files.forEach((file) => {
+     import(`${commandsDir}/${category}/${file}`).then((command) => {;
- for (let i = 0; i < Object.keys(commands).length; i++) {
-   const command = Object.values(commands)[i].default;

-   client.commands.set(command.name, command);
+       client.commands.set(command.name, command);
+    });
    // ...

You'll have to delete the index.ts files in the respective directories for this to work. Also, this would obviously make part 1 of this obsolete.

Typing

Since we're dynamically requiring, none of the imported objects will have types. If you care about not having anys in your code, you'll want to export the command interface from bot.ts and assign it to command, and make a new event interface for events.

Slash Command Registering

Don't use // @ts-ignore: Object is possibly 'null'.. There's already a feature for this. If you put a ! after a variable that is possibly null, ts will get off your back.

let test: number | null = 1;

test.toString() // error
test!.toString() // no error

But that doesn't matter, because:

Why are you using axios? You're using v13, d.js already has a function for this. You should uninstall axios and @types/axios.

slashCommands.forEach(v => {
-   // @ts-ignore: Object is possibly 'null'.
-   axios.post(`https://discord.com/api/v9/applications/${client.user.id}/commands`, v, {
-      headers: {
-        Authorization: `Bot ${client.token}`
-      }
-    }).catch(e => {
-      console.log("Error in creating slash commands.");
-      console.log(e.response.data.errors._errors);
-    })
+  client.application!.commands.create(v);
});

But wait, this is also a bad idea. Sending potentially tens to hundreds of requests in parallel will absolutely get you ratelimited. Instead, you can do it all in one request:

- slashCommands.forEach(v => {
-   client.application!.commands.create(v);
- });
+ client.application!.commands.set(slashCommands);

Bam! Note that for both of these to work, you'll need to type the slashCommands array.

+ import type { ApplicationCommandData } from 'discord.js';
// ...

- const slashCommands = [];
+ const slashCommands: ApplicationCommandData[] = [];

...useless: Array<any>

You use this a lot in your code and I don't understand why. You don't need it, at you're obviously not using it.

Naming Convention

Classes, such as bot, and interfaces, such as command, should always be pascal-case. bot -> Bot and command -> Command

bot

Categories

First of all, typo. categoires -> categories. Second of all, as I said, stuff like this doesn't scale. You should get the category list dynamically.

+ import { readdirSync } from 'fs';
+ import { join } from 'path';
// ...

- this.categories = ['general'];
+ this.categories = readdirSync(join(__dirname, './commands'));

Properties

You should follow the DRY formula and not type and assign properties. Just assign, and have typescript infer the type. (Also, never use String as a type. Always use string.

class Bot extends Client {
    // Defining the custom properties
-   commands: Collection<string, command>;
+  commands = new Collection<string, Command>;
-   categories: Array<String>;
+ categories = readdirSync(join(__dirname, './commands'));
-   commandAliases: Collection<string, string>;
+  commandAliases: new Collection<string, string>;

    constructor(options: ClientOptions) {
        super(options);
-
-       this.commands = new Collection();
-       this.categories = readdirSync(join(__dirname, './commands'));
-       this.commandAliases = new Collection();
    }
}

Since your constructor is only calling super, you can omit it entirely (and remove the ClientOptions import.

Command interface

The run function needs a type more specific type than Function. Also, you can export it, and then assign it to the command object in every command, which will remove the need to specify the argument types in every single function.

- interface Command {
+ export interface Command<Slash extends boolean> {
    name: string,
    description: string,
    category: string,
-   slash: boolean,
+ slash: Slash,
-  args: Array<string>,
+  args?: Array<Slash extends true ? CommandInteractionOption: string>,
    aliases?: Array<string>,
    timeout?: number,
    run: (client: Bot, input: Slash extends true ? CommandInteraction: Message, args: Array<Slash extends true ? CommandInteractionOption: string>) => unknown;
}
- import { CommandInteraction, CommandInteractionOption, Message } from "discord.js";
- import bot from '../../classes/bot';
+ import type { Command } from 'discord.js';

- export default {
+ export default ({
    // ...

    // The main method
-   run: async (client: bot, message: Message | CommandInteraction, args: Array<String | CommandInteractionOption>) => {
+   run: async (client, message, args) => {
        message.reply({ content: `Pong is ${client.ws.ping}` });
    }
} as Command<false>)

message will automatically be cast as a Message or CommandInteraction (same idea for args) as long as you specify whether it's a slash command in the generic, so there will be no need to cast. You'll need to set commands = Collection<string, Command> to commands = Collection<string, Command<boolean>>

All of the above can be repeated with events.

Useless .map

 const args: Array<CommandInteractionOption> = [];

// Adding all the arguments
interaction.options.data.forEach(v => args.push(v));

This is absolutely useless. If you're adding all of one array's elements to an empty array, they're both going to have the same elements. What's the point?

-  const args: Array<CommandInteractionOption> = [];

- // Adding all the arguments
- interaction.options.data.forEach(v => args.push(v));

// ...

- command.run(client, interaction, args);
+ command.run(client, interaction, interaction.options.data);

Ending Notes

You really should add in eslint and prettier with the template

KartikeSingh commented 3 years ago

thanks a lot for this detailed review, sorry i didnt saw this before, btw i am working on the newer version and i will follow make the required changes , and again thank you very much, it will be really helpful for me

btw sadly when i made this i didnt new about import( ) so yea the loading command and event this is totally shit, but i made some changes also the ...args thing is added sorry for that weird shit

KartikeSingh commented 3 years ago

well luckily i recently worked on this repo, So i got some more idea about TS and did some imporvements

KartikeSingh commented 3 years ago

Just added the new version.

Lioness100 commented 3 years ago

Looks a lot better! I added a few comments to the commit for my personal pointers (that you obviously don't have to pay any attention to if you don't want to).

I also recommend mentioning that users shouldn't npm run dev to run their bot in prod in the README, as ts-node is pretty bad performance-wise in prod and should only be used while actively developing. Instead, opt for npm run build && npm start