Did you know that you can get VS Code IntelliSense with vanilla JavaScript? By using JSDoc annotations, VS Code's TypeScript Language Server can understand your code without it actually being strictly typed.
I use JSDoc extensively in the codebase for my Discord bot, Aquarius. As Aquarius grew in size, it became harder to remember function names and parameters. Problems that, you know, a type system would solve. I have a strong preference for dynamic languages though, and didn't want to rewrite my codebase in TypeScript. JSDoc offers me some benefits of a strictly typed system without having to fully switch to one. Plus, using JSDoc forces me to document my code - which I think I read once is a recommended practice or something?
Using JSDoc
You can find additional information by reading the JSDoc documentation and on the TypeScript reference page. Note that not all JSDoc parameters are currently supported!
Documenting Classes and Variables
You can document both class variables and methods with types and descriptions. As an added benefit, this enables "Go to Definition" by ctrl/cmd+clicking usages throughout your codebase.
javascript
1class Aquarius {2 constructor() {3 /**4 * Database connection for Aquarius5 * @type {PrismaClient}6 */7 this.database = database;89 /**10 * Manages the Guilds for which Aquarius is a member11 * @type { typeof import('./core/managers/guild-manager') }12 */13 this.guildManager = new GuildManager();1415 /**16 * Stores interaction handlers17 * @type {Map<string, SlashCommandBuilder>}18 */19 this.applicationCommands = new Map();2021 /**22 * Stores interaction handlers23 * @type {Map<string, (CommandInteraction) => unknown}>}24 */25 this.applicationHandlers = new Map();26 }27}
Documenting Functions
For functions, I list the parameter types, return types, and default values.
javascript
1 /**2 * Get the list of Global Command Names3 * @param {boolean} [includeHidden=true] - Whether to include hidden commands4 * @return {string[]} List of Global Command Names5 */6 getGlobalCommandNames(includeHidden = true) {7 return Array.from(this.help.entries())8 .filter(([, info]) => info.global && (includeHidden || !info.hidden))9 .map(([key]) => key.toLowerCase());10 }
Importing Definitions across Files
For core classes and functions I need to reference from elsewhere, I use inline imports or typedef imports. I also import annotations from npm packages this way!
javascript
1/**2 * @typedef {import('./aquarius').Aquarius} Aquarius3 * @typedef {import('./core/commands/settings').default} Settings4 * @typedef {import('./core/commands/analytics').default} Analytics5 * @typedef {import('discord.js').Message} Message6 */78class SettingsManager {9 constructor() {10 /**11 * Manages the Guilds for which Aquarius is a member12 * @type { typeof import('./core/managers/guild-manager') }13 */14 this.guildManager = new GuildManager();1516 /**17 * The configured guild settings18 * @type {Settings}19 */20 this.settings = new Settings();21 }22}
Defining Complex Objects
I define a handful of complex types that I use throughout the codebase for Aquarius in a typedefs.js
file (I know, I know).
javascript
1/**2 * Description of the exported Info object by Commands and Plugins3 * @typedef {Object} CommandInfo4 * @property {string} name - The name of the Command5 * @property {string} description - A short description of the Command's function6 * @property {string} [usage] - A DocOpt usage description7 * @property {number[]} [permissions] - An array of required permissions for the command8 * @property {boolean} [hidden] - Whether the command should show up in the command list or not9 * @property {boolean} [disabled] - Whether the command is currently disabled or not10 * @property {boolean} [deprecated] - Whether the command is deprecated or not11 */1213/**14 * Description of a CommandParameter passed into Plugins and Commands15 * @typedef {Object} CommandParameter16 * @property {Aquarius} aquarius - reference to the Aquarius Client17 * @property {Settings} settings - modify settings for the command18 * @property {Analytics} analytics - track events for the command19 */2021/**22 * Function that defines commands and plugins23 * @typedef {({ aquarius, settings, analytics}: CommandParameter) => null} Command24 */
If you already use TypeScript, this post isn't meant to convince you to stop. But if you're like me and don't enjoy typed languages, give JSDoc annotations a shot. You might really like them!