@yaebal/callback-data
typed callback_data — pack and unpack button payloads with a field schema and full type inference.
install
pnpm add @yaebal/callback-datausage
call callbackData(prefix, schema) once per namespace. the schema maps field
names to one of three codecs: Number, String, or Boolean. types are inferred — pack and unpack both carry the exact shape.
import { Bot } from "@yaebal/core";
import { InlineKeyboard } from "@yaebal/keyboard";
import { callbackData } from "@yaebal/callback-data";
// define a namespace: prefix + field→codec schema
const userAction = callbackData("user", {
id: Number,
action: String,
admin: Boolean,
});
const bot = new Bot(process.env.BOT_TOKEN!);
bot.command("manage", (ctx) => {
const kb = new InlineKeyboard()
.text("ban", userAction.pack({ id: 42, action: "ban", admin: false }))
.text("promote", userAction.pack({ id: 42, action: "promote", admin: true }))
.build();
return ctx.reply("choose:", { reply_markup: kb });
});
// use the pattern to route only this namespace
bot.callbackQuery(userAction.pattern, (ctx) => {
const payload = userAction.unpack(ctx.callbackQuery.data ?? "");
if (!payload) return; // wrong namespace — shouldn't happen with pattern routing
ctx.reply(`id=${payload.id} action=${payload.action} admin=${payload.admin}`);
});routing with filter
use .filter() as a manual guard when you can't use .pattern:
// manual guard without pattern routing
bot.on("callback_query:data", (ctx) => {
if (!userAction.filter(ctx.callbackQuery.data ?? "")) return;
const payload = userAction.unpack(ctx.callbackQuery.data ?? "")!;
// payload.id is number, payload.action is string, payload.admin is boolean
});prefix-only schema
pass an empty schema {} for buttons that carry no payload:
// a namespace with no fields — useful for simple action buttons
const ping = callbackData("ping", {});
ping.pack({}); // => "ping"
ping.unpack("ping"); // => {}
ping.filter("ping"); // => trueapi
| export | signature | description |
|---|---|---|
callbackData | (prefix: string, schema: S) => CallbackData<S> | creates a typed callback_data namespace |
CallbackData | interface | the object returned by callbackData() |
CallbackData<S>
| member | type | description |
|---|---|---|
pack | (data: Infer<S>) => string | serialize a payload into a callback_data string |
unpack | (raw: string) => Infer<S> | undefined | parse a raw string; returns undefined if prefix or arity doesn't match |
filter | (raw: string | undefined) => boolean | true if raw belongs to this namespace; safe to pass undefined |
pattern | RegExp | regex anchored to the prefix — pass to bot.callbackQuery() |
codecs
| codec | TypeScript type | decode behaviour |
|---|---|---|
Number | number | parsed with Number(); non-numeric input decodes to NaN |
String | string | URL-decoded verbatim |
Boolean | boolean | exact string "true" → true; anything else → false |
Telegram caps
the prefix must not contain
codecs are not validated on unpack. this is intentional: you only ever unpack data you packed yourself, so malformed values are not a concern in normal use.
callback_data at 64 bytes. values are
URL-encoded so the : separator is safe inside field values, but encoding
expands size — keep prefixes and values short. the prefix must not contain
: — callbackData throws immediately if it does. codecs are not validated on unpack. this is intentional: you only ever unpack data you packed yourself, so malformed values are not a concern in normal use.