Testing Gets Richer, CallbackData Gets Safer, TypeScript API Reference Launches
February 18–22, 2026
A dense week across the whole GramIO ecosystem. @gramio/test v0.3.0 ships 9 new methods — editMessage, forwardMessage, sendMediaGroup, pinMessage, clickByText, three new media senders, ChatObject.post() for channel bots, plus env.clearApiCalls() and env.lastApiCall() helpers. @gramio/callback-data v0.1.0 introduces safeUnpack() — a result-typed unpacker that never throws — and makes adding optional fields to existing schemas fully backward-compatible. A brand-new @gramio/schema-parser package replaces the Rust-based tg-bot-api crate powering @gramio/types, bringing precise InputFile | string unions, semantic type markers, a synthesized Currencies enum including XTR, and the previously missing APIResponse* types. On the unreleased front: @gramio/composer gets filter-only .on() with CompatibleEvents<TEventMap, Narrowing> auto-discovery and a typed Patch generic on use(); gramio itself gains filter-only .on() overloads and a full filters.* predicate library with forwardOrigin(type?) and senderChat(type?) narrowing. The documentation push is equally large — a full TypeDoc API reference at /api/, 200+ Telegram Bot API method pages at /telegram/, a new Cheat Sheet, JSX and Split plugin pages, interactive middleware and broadcast visualizers, a Composer production architecture guide, expanded session and formatting anti-pattern docs, and a global copy-as-Markdown button on every page.
@gramio/test v0.3.0 — More Actors, More Actions
9 new methods: edit, forward, pin, media group, and clickByText
The test library got a significant API expansion. Here's everything that shipped:
user.editMessage(msg, text) — emits an edited_message update. Test edit handlers without boilerplate:
const msg = await user.sendMessage("Hello");
await user.editMessage(msg, "Hello, world!");
expect(env.apiCalls[0].method).toBe("editMessageText");user.forwardMessage(msg, toChat?) — emits a message update with forward_origin set. Useful for testing forward-detection logic:
const msg = await user.sendMessage("Original message");
await user.forwardMessage(msg, group); // forwarded to groupuser.sendMediaGroup(chat, payloads[]) — emits multiple message updates sharing the same media_group_id. Tests album handling:
await user.sendMediaGroup(group, [
{ photo: {} },
{ video: {} },
]);user.pinMessage(msg, inChat?) — emits a service message with pinned_message set. GramIO routes these to the "pinned_message" event:
const msg = await user.sendMessage("Important announcement");
await user.pinMessage(msg);user.on(msg).clickByText(buttonText) — scans the message's inline_keyboard for a button whose text matches, then clicks its callback_data. Throws if no match is found:
// (bot replies with an inline keyboard: "Yes" and "No" buttons)
await user.on(botReply).clickByText("Yes");Three new media send methods: sendAudio(), sendAnimation(), sendVideoNote() — with auto-generated file IDs and the same API as existing media methods:
await user.sendAudio({ caption: "Great track" });
await user.sendAnimation(group, { caption: "Funny gif" });
await user.sendVideoNote(); // circle videoChatObject.post(text) — emits a channel_post update (no from field) for testing channel bots:
const channel = env.createChat({ type: "channel", title: "My Channel" });
await channel.post("New post from channel");env.clearApiCalls() and env.lastApiCall()
Two new environment helpers for cleaner assertions:
// Reset the call log between test phases
env.clearApiCalls();
// Get the last call for a specific method
const lastSend = env.lastApiCall("sendMessage");
expect(lastSend?.params.text).toBe("Welcome!");@gramio/callback-data v0.1.0 — safeUnpack() & Backward-Compatible Schemas
safeUnpack() — never crash on stale buttons
Inline keyboard buttons live in Telegram's chat history for days or weeks. If your schema changes in the meantime, unpack() throws on old buttons. The new safeUnpack() wraps it and never throws — instead it returns a typed discriminated union.
Note: If you use
bot.callbackQuery(schema, handler), unpacking is done automatically — you getctx.queryDatafully typed and never needsafeUnpack. It's useful when you're handlingcallback_querywithbot.on()or want to support multiple schema versions in one handler.
const data = new CallbackData("action").number("id").string("type");
// Only useful outside bot.callbackQuery() — e.g. a generic catch-all handler
bot.on("callback_query", (ctx) => {
const result = data.safeUnpack(ctx.data ?? "");
if (!result.success) {
// button from an old schema version
return ctx.answerCallbackQuery({ text: "This button is outdated!" });
}
ctx.answerCallbackQuery({ text: `ID: ${result.data.id}` }); // fully typed
});The return type is SafeUnpackResult<T> — exported for your own type-guard usage.
Adding optional fields is now backward-compatible
Previously, adding any field to a schema (even optional ones) would break existing packed strings. Now, when deserializing data that was packed without a bitmask, all optional fields are treated as absent — giving them their default or undefined. Adding optional fields to the end of a schema is now a safe migration:
// v1 schema (packed strings already exist in chat history)
const v1 = new CallbackData("page").number("id");
// v2 schema — adding optional field is safe!
const v2 = new CallbackData("page")
.number("id")
.string("tab", { optional: true }); // new field
// Old v1-packed strings unpack fine in v2
const result = v2.safeUnpack(v1PackedString);
// result.success === true, result.data.tab === undefinedBreaking schema operations (adding required fields, changing field order, renaming nameId) are still unsafe — document your schema history accordingly.
@gramio/schema-parser v1.0.1 — New Internal Schema Engine
A new TypeScript package that parses the Telegram Bot API documentation HTML into a structured, type-annotated schema. It powers @gramio/types — replacing the previous dependency on the Rust-based tg-bot-api crate.
The schema supports:
- Semantic type markers —
semanticType: "formattable"for text fields,"markup"for keyboard objects,"updateType"for context types - File upload detection — string fields accepting file uploads become
InputFile | stringunions - Currency enum — synthesizes a
Currenciestype from Telegram'scurrencies.json, includingXTR(Telegram Stars) - Proper const values — boolean literals
true/falseinstead of generic strings - oneOf support — union types with links and descriptions parsed correctly
@gramio/types v9.4.2 — Native Schema, Better Semantic Types
Migrated from tg-bot-api (Rust crate) to @gramio/schema-parser (TypeScript). The generated output now has:
- More precise
InputFile | stringunions for file upload parameters - Formattable fields typed via semantic markers
Currenciesenum covering all supported currencies includingXTRfor Stars paymentsAPIResponse,APIResponseOk,APIResponseErrortypes added (previously missing)
@gramio/composer — Filter-Only .on() & CompatibleEvents (Upcoming)
New features landed in composer's main branch alongside the gramio filter work:
Filter-only .on() overload in Composer
The composer now supports calling .on(filter, handler) without an event name. A CompatibleEvents<TEventMap, Narrowing> utility type auto-discovers which events contain all the keys the predicate narrows to:
// Type-narrowing predicate — composer finds matching events automatically
app.on(
(ctx): ctx is { text: string } => typeof ctx.text === "string",
(ctx) => {
ctx.text; // string — narrowed, and handler only fires on events that have .text
}
);Patch generic on use()
use<Patch extends object>(handler) now accepts a type parameter for local context extension without changing TOut for downstream middleware:
app.use<{ requestId: string }>((ctx, next) => {
ctx.requestId = crypto.randomUUID();
return next();
});gramio — New Filter System (Upcoming)
Not in a release yet, but significant filter work landed on main:
Filter-only .on() overloads
Call .on() with just a predicate — without specifying an event name. GramIO auto-discovers compatible events from the type-narrowing predicate:
// Type-narrowing predicate — narrows context type in the handler
bot.on((ctx): ctx is { text: string } => typeof ctx.text === "string", (ctx) => {
ctx.text; // string (not string | undefined)
});New message property filters
Standalone predicate functions for type-narrowing in handlers:
import { filters } from "gramio";
bot.on("message", filters.reply, (ctx) => {
ctx.replyToMessage; // guaranteed to exist
});
bot.on("message", filters.entities, handler); // has entities
bot.on("message", filters.quote, handler); // has quoted reply
bot.on("message", filters.isBot, handler); // from is a bot
bot.on("message", filters.isPremium, handler); // sender has PremiumforwardOrigin() and senderChat() as callable overloads
Previously separate filters, now a single callable overload with optional type narrowing:
// Narrows to messages forwarded from a user
bot.on("message", filters.forwardOrigin("user"), (ctx) => {
ctx.forwardOrigin; // MessageOriginUser — fully typed
});
// No argument — matches any forwarded message
bot.on("message", filters.forwardOrigin(), handler);Documentation
This period saw a large documentation push — new guides, interactive visualizers, and a full Telegram Bot API reference built from scratch.
TypeScript API Reference launched at /api/
A full TypeDoc-generated API reference is now at /api/. It covers all core and plugin packages — gramio, @gramio/contexts, @gramio/types, @gramio/keyboards, @gramio/callback-data, @gramio/composer, and a dozen plugins. The sidebar is auto-generated and flattens internal module paths for readability.
Telegram Bot API Reference — 200+ methods at /telegram/
Over 200 Telegram Bot API methods and types now have dedicated pages. Each page has GramIO usage examples, parameter tables (with constraints, types, enum values, and required/optional indicators), error tables, and related method cross-references. The ApiParam component renders inline type badges that link to related Telegram types. Clickable type badges, italic True/False literals, multipart badges, and docsLink annotations for Files/Keyboards/Formattable cross-references were all added this cycle (da3a4eb, 526b4db, b2167cc).
Cheat Sheet page
A quick-reference cheat sheet is now in the nav at /cheat-sheet. Common GramIO patterns in one place — no more digging through guide pages for the right method signature.
JSX plugin documentation
New page at /plugins/official/jsx covering the @gramio/jsx plugin — text formatting, inline and reply keyboard builders with JSX, and bilingual usage examples.
Split plugin documentation expanded
The @gramio/split page at /plugins/official/split now has full installation steps, detailed API reference, entity splitting mechanics explanation, custom limit examples, and framework-agnostic usage patterns.
Interactive middleware pipeline visualizer
The middleware guide grew an interactive UpdatePipelineVisualizer component — toggle update types, simulate debug mode, and watch the context flow in real time without reading walls of text.
Rate limits docs — BroadcastVisualizer & deep dive
The rate limits page got a BroadcastVisualizer component and a detailed breakdown of HTTP 429 mechanics, broadcast anti-patterns (the naive for loop), and a comparison of simple loop vs @gramio/broadcast vs BullMQ — with timing diagrams.
@gramio/composer production architecture guide
The composer reference now covers Named Composer deduplication mechanics, isolation group internals, scoped/global propagation patterns, and the dedup/shared-data gotcha. A production architecture pattern with withUser/router assembly is included (bcc0c8f).
Formatting anti-patterns
Critical rules added to the formatting guide:
// ❌ WRONG — never use parse_mode with @gramio/format entities
ctx.send({ text: format`${bold("hi")}`, parse_mode: "HTML" });
// ❌ WRONG — never .join() Formattable arrays (loses entity offsets)
[bold("a"), italic("b")].join(", ");
// ✅ CORRECT — use the join() helper from @gramio/format
import { join } from "gramio";
join([bold("a"), italic("b")], ", ");Session plugin docs expanded
The session docs now cover lazy loading, $clear() for session deletion, custom session key functions for per-chat vs per-user-per-chat scoping, and explicit TypeScript typing for both eager and lazy session modes.
Copy/Download as Markdown on every page
The CopyOrDownloadAsMarkdownButtons component is now globally registered — every documentation page has a one-click button to copy or download its content as clean Markdown, useful for feeding docs into AI assistants.