typed examples
yaebal's main feature is not just runtime convenience. it is the way the context type changes as you build the chain. these examples show the intended typescript shape.
filter queries narrow context
a query like message:text is both a runtime route and a type-level narrowing rule.
bot.on("message:text", (ctx) => {
ctx.text;
// ^ string, narrowed by the filter query
});
bot.on("callback_query:data", (ctx) => {
ctx.callbackQuery.data;
// ^ string | undefined on the raw payload, but callbackQuery itself is guaranteed
});derive and decorate accumulate
decorate adds static fields and derive adds per-update fields. handlers
downstream see both.
const bot = createBot(token)
.decorate({ appName: "shop" })
.derive(async (ctx) => ({ user: await loadUser(ctx.from?.id) }))
.on("message:text", (ctx) => {
ctx.appName;
// ^ "shop"
ctx.user;
// ^ Awaited<ReturnType<typeof loadUser>>
ctx.text;
// ^ string
});plugin-added context stays typed
the session shape comes from the generic you pass to session() and flows into later
handlers.
interface SessionData {
cart: string[];
}
const bot = createBot(token)
.install(session<SessionData>({ initial: () => ({ cart: [] }) }))
.command("cart", (ctx) => {
ctx.session.cart.push("sku_1");
// ^ string[]
});plugin dependencies are explicit
a plugin can say which context fields it requires. installing it too early becomes a compile-time error instead of a hidden middleware-order bug.
type NeedsSession = Context & { session: { userId?: number } };
function currentUser(loadUser: (id: number) => Promise<User>): Plugin<NeedsSession, { user: User | null }> {
return (composer) => composer.derive(async (ctx) => ({
user: ctx.session.userId ? await loadUser(ctx.session.userId) : null,
}));
}
new Bot(token).install(currentUser(loadUser));
// TypeScript error: session is not installed yet.
new Bot(token)
.install(session({ initial: () => ({}) }))
.install(currentUser(loadUser));
// ok.generated contexts are runtime shortcuts
use createBot() from the meta package when you want generated per-update shortcuts at
runtime.
const bot = createBot(token);
bot.on("message:text", (ctx) => {
ctx.react("🔥");
// ^ generated MessageContext shortcut
});
bot.on("callback_query:data", (ctx) => {
ctx.answer("ok");
// ^ generated CallbackQueryContext shortcut
});feature composers inherit types
build features as plain composers, extend shared plugin setup, then attach them to the bot. the context type follows the chain.
const shared = new Composer()
.install(session({ initial: () => ({ count: 0 }) }))
.decorate({ feature: "shared" });
const feature = new Composer()
.extend(shared)
.command("count", (ctx) => {
ctx.session.count;
ctx.feature;
});
createBot(token).extend(shared).extend(feature);declare module just to make ctx.foo exist. add it with derive, decorate, or a typed plugin
so both runtime and typescript stay aligned.