Skip to content

Filters

UNRELEASED

The filters system is currently on the main branch and has not been included in a tagged release yet. The API may change before the stable release.

GramIO's filter system lets you apply type-safe predicates to context — either as the third argument to bot.on(event, filter, handler) or as the only argument in a filter-only .on(filter, handler) call that auto-discovers compatible events.

Filter-Only .on() — No Event Name Required

In the standard .on(event, handler), you choose the event explicitly. With the filter-only overload you skip the event name entirely — GramIO infers which events are compatible from the shape of your predicate:

ts
import { Bot } from "gramio";

const bot = new Bot(process.env.BOT_TOKEN!);

// Boolean filter — runs on any update where ctx has `text`
bot.on((ctx) => "text" in ctx && ctx.text?.startsWith("!"), (ctx) => {
    // ctx is narrowed to the union of all events that have a `text` field
    ctx.send("Command received!");
});

// Type-narrowing predicate — narrows the handler's context type
bot.on(
    (ctx): ctx is { text: string } => typeof (ctx as any).text === "string",
    (ctx) => {
        ctx.text; // string — not string | undefined
    }
);

Inline Filter on a Named Event

You can also pass a filter as the second argument to a named .on():

ts
bot.on("message", (ctx) => ctx.text?.startsWith("/"), (ctx) => {
    // only messages starting with "/"
});

// Type-narrowing inline filter
bot.on(
    "message",
    (ctx): ctx is typeof ctx & { text: string } => typeof ctx.text === "string",
    (ctx) => {
        ctx.text; // string
    }
);

Standalone Predicate Functions

Import from gramio and pass as filters to .on() or .guard():

ts
import { filters } from "gramio";

Message Content Filters

FilterNarrows to / Guards for
filters.replyMessage has reply_to_message
filters.entitiesMessage has entities
filters.captionEntitiesMessage has caption_entities
filters.quoteMessage has a quoted reply (quote)
filters.viaBotMessage was sent via a bot (via_bot)
filters.linkPreviewMessage has link_preview_options
filters.startPayload/start with a deep-link payload
filters.authorSignatureMessage has author_signature
filters.mediaGroupMessage belongs to a media group
filters.venueMessage is a venue
ts
bot.on("message", filters.reply, (ctx) => {
    ctx.replyToMessage; // guaranteed present
});

bot.on("message", filters.startPayload, (ctx) => {
    // message is a /start with payload
});

Sender & Chat Filters

FilterDescription
filters.hasFromUpdate has a from field (user sender)
filters.isBotfrom.is_bot === true
filters.isPremiumfrom.is_premium === true
filters.isForumChat has is_forum === true
filters.serviceMessage is a service message
filters.topicMessageMessage is in a forum topic
filters.mediaSpoilerMedia has spoiler effect
filters.giveawayMessage contains a giveaway
filters.gameMessage contains a game
filters.storyMessage is a story
filters.effectIdMessage has a message effect

filters.forwardOrigin(type?) — Forward Origin Narrowing

Without an argument, matches any forwarded message. With a type argument, narrows the forwardOrigin type:

ts
// Any forwarded message
bot.on("message", filters.forwardOrigin(), (ctx) => {
    ctx.forwardOrigin; // MessageOrigin (union of all origin types)
});

// Narrowed to a specific origin type
bot.on("message", filters.forwardOrigin("user"), (ctx) => {
    ctx.forwardOrigin; // MessageOriginUser
    ctx.forwardOrigin.sender_user; // TelegramUser
});

bot.on("message", filters.forwardOrigin("channel"), (ctx) => {
    ctx.forwardOrigin; // MessageOriginChannel
    ctx.forwardOrigin.chat;         // TelegramChat
    ctx.forwardOrigin.message_id;   // number
});

Available types: "user" | "hidden_user" | "chat" | "channel"

filters.senderChat(type?) — Sender Chat Narrowing

ts
// Any message sent on behalf of a chat (anonymous admin, channel)
bot.on("message", filters.senderChat(), (ctx) => {
    ctx.senderChat; // TelegramChat
});

// Narrowed to a specific chat type
bot.on("message", filters.senderChat("channel"), (ctx) => {
    ctx.senderChat; // TelegramChat & { type: "channel" }
});

Composing Filters

Filters are plain functions — compose them with boolean logic:

ts
const isPremiumAdmin = (ctx: any) =>
    filters.isPremium(ctx) && filters.hasFrom(ctx) && ctx.from.status === "administrator";

bot.on("message", isPremiumAdmin, handler);

Type-Narrowing guard()

guard() with a type predicate narrows TOut for all downstream middleware in the chain:

ts
const bot = new Bot(process.env.BOT_TOKEN!);

bot
    .guard((ctx): ctx is typeof ctx & { text: string } => typeof ctx.text === "string")
    .on("message", (ctx) => {
        ctx.text; // string — narrowed for all handlers after the guard
    });

Without a type predicate (boolean guard), it acts as a gate that blocks the chain on false but doesn't narrow types.

See Also