Package

Discord gives a component one string to carry its state, capped at 100 characters. @seedcord/custom-id declares what that string holds, packs values into it, and reads them back with the types you declared. The component that mints the id and the code that reads a click share one declaration.
Values pack into a compact wire, which fits more of them under the cap. Every id carries a short hash of its shape, so a click on a component built before you changed the fields refuses, and the wrong values never reach your code.
When using it directly with discord.js or any other library, you'll have to set up routing yourself. Seedcord's gateway and http packages do that for you, so you can just declare the shape and read the wire.
Until v1.0.0, minor versions can break.
pnpm add @seedcord/custom-idA seedcord bot already has this through @seedcord/gateway or @seedcord/http. Installing it directly risks a second copy whose decode failures skip your bot's reply cards.
import { ButtonBuilder, Events } from 'discord.js';
import { CustomId } from '@seedcord/custom-id';
const Approve = new CustomId('approve').snowflake('userId').oneOf('action', ['approve', 'deny']);
new ButtonBuilder().setCustomId(Approve.encode({ userId: '123', action: 'deny' })).setLabel('Deny');
client.on(Events.InteractionCreate, (interaction) => {
if (!interaction.isButton()) return;
if (!Approve.owns(interaction.customId)) return;
const { userId, action } = Approve.decode(interaction.customId);
// userId: string, action: 'approve' | 'deny'
});Fields come from snowflake, uuid, int, bool, oneOf, and str. Each one takes { nullable: true } to also carry null. A bounded int('page', 1, 50) packs into fewer characters than an unbounded one.
prefixOf recovers the route prefix from a raw wire. The prefix survives a shape change, which makes it what you route on. decodeFor takes several definitions at once and returns the matched prefix with its own params.
Refer to the guide for more examples, and the reference for the full API.
decode throws two things. A wire minted before the shape changed throws CustomIdWireStale. A corrupt wire, or one another definition minted, throws CustomIdWireInvalid. Both are SeedcordError from @seedcord/errors, so branch on the code.
setCustomIdErrors replaces both with your own constructors. Replace them with Notice subclasses when using the seedcord framework.