Plugins
Plugins extend Player with custom logic and typed events. They’re the right tool when you need to tap into the node dispatch pipeline, react to payloads from a Lavalink server plugin, or add behaviour that spans multiple queues without cluttering your command handlers.
The structure
Section titled “The structure”A plugin is any object that satisfies the PlayerPlugin interface:
interface PlayerPlugin<EventMap extends Record<string, unknown[]> = {}> { readonly _: EventMap; readonly name: string; init(player: Player): void;}Three members to implement:
-
name— areadonlystring literal. TypeScript uses the literal type to keyplayer.plugins, soreadonly name = "myplugin"gives youplayer.plugins.myplugin. A non-literal type (e.g.name: string) widens and breaks that inference. -
init(player)— called once duringplayer.init(), after all nodes are created but before they connect. This is where you register event listeners. Node REST calls should wait fornodeReadysince nodes aren’t connected yet. -
_— a phantom type field. Declare it withdeclare readonly _: YourEventMapso TypeScript can extract your event map and surface it on the player’son/emit. It has no runtime existence —declaremeans no value is emitted. Without it, your events won’t appear on the player.
Implementing a plugin
Section titled “Implementing a plugin”No events
Section titled “No events”If your plugin only reacts to things without emitting its own events, pass {} as the event map and declare _ accordingly:
import type { Player, PlayerPlugin } from "discolink";
export class MyPlugin implements PlayerPlugin { readonly name = "myplugin"; declare readonly _: {};
init(player: Player): void { player.on("trackStart", (queue, track) => { // your logic }); }}With typed events
Section titled “With typed events”Define your event map as a type where each key is an event name and each value is a tuple of argument types. Then pass it as the generic argument and declare _ against it:
import type { Player, PlayerPlugin, Queue } from "discolink";
type MyEventMap = { myEvent: [queue: Queue, data: string];};
export class MyPlugin implements PlayerPlugin<MyEventMap> { readonly name = "myplugin"; declare readonly _: MyEventMap;
init(player: Player): void { player.on("nodeDispatch", this.#onDispatch as any); }
#onDispatch(this: Player<{}, MyPlugin[]>, _node: Node, payload: unknown) { // narrow payload to what your Lavalink plugin emits if (!isMyPayload(payload)) return; const queue = this.getQueue(payload.guildId); if (!queue) return; this.emit("myEvent", queue, payload.data); }}Events are emitted on the Player instance, not the plugin itself. Consumers subscribe the usual way:
player.on("myEvent", (queue, data) => { ... });Typing this in dispatch handlers
Section titled “Typing this in dispatch handlers”When a handler needs to call player methods or access this.plugins, annotate this explicitly with the plugin in the tuple:
#onDispatch(this: Player<{}, MyPlugin[]>, _node: Node, payload: MyPayload) { const queue = this.getQueue(payload.guildId); if (!queue) return; this.emit("myEvent", queue, payload.data);}If you need to access your own plugin instance (e.g. to read options), this.plugins.myplugin gives you back the typed instance:
#onDispatch(this: Player<{}, MyPlugin[]>, _node: Node, payload: MyPayload) { const { threshold } = this.plugins.myplugin; // ...}Registering a plugin
Section titled “Registering a plugin”Pass plugin instances to the plugins option when constructing Player. TypeScript infers the tuple and merges all event maps automatically:
const player = new Player({ plugins: [new MyPlugin()], nodes: [ /* ... */ ], async forwardVoiceUpdate(guildId, payload) { /* ... */ },});
// fully typed — plugins record and events both resolved from the tupleplayer.plugins.myplugin;player.on("myEvent", (queue, data) => { ... });