Code Quality
TypeScript patterns, naming conventions, code organization, and clean code guidelines for Plexcord plugin development.
Code Quality
Writing maintainable plugin code is as important as writing working code. These guidelines help you produce plugins that are easy to understand, test, and update.
Use TypeScript Fully
Plexcord is TypeScript-first. Lean on it completely.
Always type your data models
// ✅ Good: clear, self-documenting
interface SavedMessage {
id: string;
content: string;
authorId: string;
savedAt: number;
}
// ❌ Avoid: all information lost
const data: any = { id: "...", content: "..." };Type-safe settings access
When you use definePluginSettings, the returned object is fully typed:
const settings = definePluginSettings({
maxItems: { type: OptionType.NUMBER, description: "...", default: 10 }
});
// settings.store.maxItems is typed as number
const limit: number = settings.store.maxItems;Avoid as any in patches
In patches' replacement strings you sometimes must use runtime code that bypasses type checking. Keep these sandboxed:
// Acceptable: isolated to the replacement string
replacement: {
replace: "$self.handleRender($1.props)"
}
// Plugin method is properly typed
handleRender(props: MessageProps): string {
return props.content.toUpperCase();
}Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Plugin file | camelCase folder | src/plugins/myPlugin/index.ts |
| Plugin name | PascalCase string | name: "MyPlugin" |
| Settings variable | settings (always) | const settings = definePluginSettings(...) |
| Logger variable | logger (always) | const logger = new Logger("MyPlugin") |
| DataStore keys | PluginName_keyName | "VoiceLogger_history" |
| Flux handler props | Destructure what you need | ({ message, channelId }) |
File Organization
For simple plugins, a single index.ts is enough:
src/plugins/myPlugin/
└── index.tsFor complex plugins:
src/plugins/myPlugin/
├── index.ts ← Plugin entry, exports definePlugin
├── settings.ts ← definePluginSettings
├── components/
│ └── SettingsPanel.tsx
├── utils.ts ← Helper functions
└── types.ts ← TypeScript interfacesLogger Usage
Always use Logger instead of console.log:
import { Logger } from "@utils/Logger";
const logger = new Logger("MyPlugin");
// ✅ Good
logger.info("Processing", count, "messages");
logger.error("Failed to save:", error);
logger.debug("State:", JSON.stringify(state));
// ❌ Avoid: no context, hard to filter in DevTools
console.log("Processing messages");
console.error(error);In DevTools, you can filter by your plugin's prefix: type MyPlugin in the console filter.
Clean Cleanup in stop()
Every resource your plugin claims in start() must be released in stop():
start() {
// Register event listeners
document.addEventListener("keydown", this.keyHandler);
// Store interval reference
this.updateInterval = setInterval(() => this.update(), 5000);
}
stop() {
// ✅ Remove listener
document.removeEventListener("keydown", this.keyHandler);
// ✅ Clear interval
clearInterval(this.updateInterval);
this.updateInterval = null;
}If you don't clean up:
- Disabled plugins continue consuming memory and CPU
- Event handlers fire after the plugin is off
- Users must restart Discord to fully unload your plugin
Flux Error Handling
Flux event handlers run in Discord's event loop. An unhandled error can break all subsequent events for that type across all plugins. Always wrap your handlers:
flux: {
MESSAGE_CREATE({ message }) {
try {
this.processMessage(message);
} catch (e) {
logger.error("Unhandled error in MESSAGE_CREATE:", e);
}
}
}Idiomatic Plugin Structure
import definePlugin, { OptionType } from "@utils/types";
import { definePluginSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";
// 1. Logger at the top
const logger = new Logger("MyPlugin");
// 2. Settings next
const settings = definePluginSettings({
enabled: {
type: OptionType.BOOLEAN,
description: "Enable feature X",
default: true
}
});
// 3. Pure utility functions (not on the plugin object)
function computeSomething(input: string): string {
return input.trim();
}
// 4. Main plugin export
export default definePlugin({
name: "MyPlugin",
description: "Does something useful",
authors: [{ name: "YourName", id: 0n }],
settings,
start() {
logger.info("Started");
},
stop() {
logger.info("Stopped");
},
flux: {
MESSAGE_CREATE({ message }) {
if (!settings.store.enabled) return;
try {
const result = computeSomething(message.content);
logger.debug("Result:", result);
} catch (e) {
logger.error("Error:", e);
}
}
}
});Localize User-Facing Strings
Any string that appears in the UI (settings labels, notification bodies, button text) should use t() instead of a hardcoded value. This makes your plugin work for users in any supported language without any extra effort.
import { t, plugin } from "@api/i18n";
// ✅ Good: automatic Turkish, English, and future language support
export default definePlugin({
name: "MyPlugin",
description: () => t(plugin.myPlugin.description),
settings: definePluginSettings({
feature: {
label: () => t(plugin.myPlugin.settings.featureLabel),
description: () => t(plugin.myPlugin.settings.featureDesc),
type: OptionType.BOOLEAN,
default: true
}
}),
start() {
showNotification({
title: t(plugin.myPlugin.notif.title),
body: t(plugin.myPlugin.notif.body)
});
}
});
// ❌ Avoid: hardcoded English, users in other locales get no translation
export default definePlugin({
name: "MyPlugin",
description: "Does something useful",
settings: definePluginSettings({
feature: {
label: "Enable feature",
description: "Toggles the main feature on or off",
type: OptionType.BOOLEAN,
default: true
}
})
});Key rules:
- Always wrap
t()in a function (() => t(...)) insidedefinePlugin; strings are evaluated at render time, not at module load - Use proxy key access (
plugin.myPlugin.label) instead of string literals, for IntelliSense and type safety - Add your strings to
src/locales/en.tsunderplugin.yourPluginName.*, then mirror them insrc/locales/tr.ts
See the i18n API reference for the full guide.