@yaebal/filters

composable, type-narrowing update filters (the mtcute idea) for the core composer.filter(...) method — combine with and / or / not, and some attach typed data to the context.

install

terminal
pnpm add @yaebal/filters

the filter() method

filter() lives in core. It runs its handlers only when the filter matches, and because a filter is a type guard, the handler context is narrowed to C & Add — including any data the filter attached.

filter.ts
// composer.filter(filter, ...handlers) — runs handlers only when the filter matches.
// the filter is a TYPE GUARD, so handlers see C & Add:
bot.filter(command("add"), (ctx) => {
  ctx.command; // string
  ctx.args;    // string[]   ← added by the filter, fully typed
});

usage

bot.ts
import { and, or, not, command, regex, isPrivate, fromUser, mediaType } from "@yaebal/filters";

bot.filter(and(isPrivate, command("buy")), (ctx) => ctx.args);   // ctx.args: string[]
bot.filter(regex(/^\d+$/), (ctx) => ctx.match[0]);              // ctx.match: RegExpMatchArray
bot.filter(or(mediaType("photo"), mediaType("video")), handler);
bot.filter(not(fromUser(BANNED_ID)), handler);

built-in filters

filtermatchesattaches
textnon-empty message textctx.text: string
regex(re)text matches rectx.match: RegExpMatchArray
command(name?)a /commandctx.command, ctx.args
chatType(...t)chat type in t
isPrivate / isGroupshorthand chat types
fromUser(...ids)sender id in ids
chatId(...ids)chat id in ids
mediaType(...k)message has media kind k
mediaany media
hasEntity(type)message has an entity of type

combinators

  • and(a, b, …) — all must match; the attached additions intersect.
  • or(a, b, …) — any matches; no additions (the matched branch is unknown).
  • not(a) — inverts; no additions.
All of them are also under one namespace, mtcute-style: import { filters } from "@yaebal/filters" then filters.command(...), filters.and(...).

custom filters

A filter is just an object with a type-guard test. Return true after (optionally) attaching data — exactly how regex exposes ctx.match.

custom.ts
import type { Filter, Context } from "@yaebal/core";

// a filter is just a type guard that may attach data:
const fromAdmin: Filter<Context, { isAdmin: true }> = {
  test(ctx): ctx is Context & { isAdmin: true } {
    if (!ADMINS.has(ctx.from?.id ?? 0)) return false;
    Object.assign(ctx, { isAdmin: true });
    return true;
  },
};
bot.filter(fromAdmin, (ctx) => ctx.isAdmin);
Filter queries (on("message:text")) still exist and are great for the common case — filter() adds composition, custom predicates, and data attachment on top.