Repository navigation
Missing type definitions on ctx.message #1388
Description
Activity
- pinned this issue
on Mar 5, 2021 I'm glad you asked! I explained it a few times already, but I think about this a lot, so I have a better understanding each time.
TypeScript is trying to tell you that not every message is a text message.
There are several ways you can proceed in Telegraf 4:
- Use
bot.on('text', (ctx) => ...). It's that simple. - Narrow manually:
if (!('text' in ctx.message)) return next()(messy whenctx.messagemight beundefined). - Manually use a type guard (not included):
if (!has.text(ctx.message)) return next(). - Pass a type guard to
Composer.guard. deunionize(ctx.message).text.
What could Telegraf 5 do?
- Simply make each
Messageproperty always optional. Telegraf 3 did this, and it needs a ton of (unsafe) non-null assertions:bot.on('text', ctx => ctx.reply(ctx.message!.text!)) // typechecks, fails at runtime bot.on('photo', ctx => ctx.reply(ctx.message!.text!))
- Add
_typediscriminator property. One needs to know to check it. Needs extra code that breaks with API updates. deunionizeby default. After narrowing manually, suggestions are bloated with multiple?: undefinedproperties.
Reacted by Benny Neugebauer, Nik, lisovyk, bokssssss, Andrey, samuelquaynor, Ali Sawari, Михаил Александрович, ivanjermakov, Alen and 3 moreReacted by WooMai and Manzoor Wani- Use
You could also use ascription if you're totally sure about the message type, and this brings you zero cost at the runtime.
import { Message } from 'typegram' // ... bot.on('text', (ctx) => ctx.reply((ctx.message as Message.TextMessage).text)) // ...
I believe this is where TypeScript takes over JavaScript. An explicit type guard is necessary to force you to consider those corner cases.
Reacted by alsotang, bokssssss, zaikir, Ahmed Kamel, Mauricio Witter, Minasie Shibeshi and Aleksander BerezhnevReacted by Wojciech Pawlik, Darvesh, Anuwong Mongkudkornburee, Gilang Ramadhan, A-J Roos, Suiko, SafwanGanz, Rafael Sampaio and Yan G.Hi @wojpawlik,
this somehow plays kind of in the same league as the issue itself. Which type for ctx should I use when creating method interfaces for the command handlers? I am using:
bot.on('text', this.someKindOfHandlerFunction);
The type that ctx gets isMatchedContext<Context<Update>, "text">which by itself is not public and can't be used as a type for the ctx param in "someKindOfHandlerFunction". I am kind of confused, do you have a good idea?
When usingContext<Update>as type for ctx the information about the MatchedContext type "text" gets lost.Reacted by Marco Pinton, Dmitry Kirilyuk and Dmitriy16 remaining items
Since people are going to find this issue, the new way to filter updates is like so:
import { message } from "telegraf/filters"; bot.use(ctx => { if (ctx.has(message("text")) { ctx.message.text // works! } });
These filters are also usable in
bot.on.bot.on(message("text"), ctx => { ctx.message.text // works! });
Both
bot.onandctx.hassupport update type strings (like "message", "edited_message"), but using message types (like "text", "photo") is no longer supported. Filters replace them.You can also now filter for things you could not before, like
editedMessage(),channelPost(),editedChannelPost(), andcallbackQuery("data")/callbackQuery("game_short_name"):import { editedMessage, channelPost } from "telegraf/filters"; bot.on(channelPost("video"), ctx => { ctx.channelPost.video // works! });
This is available since 4.11.0 — I strongly recommend reading the release notes.
We will also keep working on making filters more powerful in v4, and the ctx object even more easier to use in v5.
See also: #1471
Reacted by Torben, web4ken, kimheng and Alexwould it be possible to show a typed example of: bot.on('message', (ctx) => { })
the correct typing of ctx has so far eluded me
Code from @MKRhere. Sorry for asking a silly question.
bot.on(message("text"), ctx => { ctx.message.text // works! });
If I would like to pass the ctx, what type should I use for
handleRequestparameter?bot.on(message('text'), async (ctx) => { await handleRequest(ctx); }); const handleRequest = async (ctx: ?) => { console.log(ctx.message?.text) };
import type { Update, Message } from "telegraf/types"; const handleRequest = async (ctx: Context<Update.MessageUpdate<Message.TextMessage>>) { // ... }
Reacted by Amirul, HariharanGS and SALVADOR, 1 M. LIGAYAOfor custom Context, lets say MyContext where custom
sessionpresent
when we check forif(ctx.has(message('text')){ ctx.session }Here ts complains about session does not exists.
After ctx.has(...) it converts ctx to use default Context rather that MyContext
Current:
NarrowedContext<Context, Update.MessageUpdate<Record<"text", {}> & Message.TextMessage & AddOptionalKeys>>
Desired:
NarrowedContext<MyContext, Update.MessageUpdate<Record<"text", {}> & Message.TextMessage & AddOptionalKeys>>@djeks922 I cannot reproduce this. Can you show more surrounding code?
// Define your own context type export interface MyContext extends Context { session?: SessionData; }async function usageCheck( ctx: MyContext, next: () => Promise<void> )if ( ctx.has(message("voice")) && ctx.session?.messagesCount! >= ctx.session?.maxDailyMessages! ) { return await ctx.sendMessage(freeDailyVoiceLimitResponse); }TS: Property 'session' does not exist on type 'NarrowedContext<Context<Update>, MessageUpdate<Record<"voice", {}> & VoiceMessage & AddOptionalKeys<never>>>'export interface SessionData { messagesCount: number; maxDailyMessages: number; currentTopic: string; topics: Array<string>; maxMonthlyImages: number; imagesResetDate: Date; imagesCount: number; maxDailyVoices: number; voiceCount: number , subscription: string; }fyi
I'm not good at typescript so, maybe is there some option to pass MyContext as generic T to .has or similar way?
'NarrowedContext<Context, MessageUpdate<Record<"voice", {}> & VoiceMessage & AddOptionalKeys>>'
Becausehaschanges MyContext to Context when narrowing, ignoring the parent object type, in our case MyContextThanks beforehand, I hope I was able to explain the problem
@djeks922 I was able to recreate your problem. Make
sessionnon-optional, and make it| undefined:export interface MyContext extends Context { - session?: SessionData; + session: SessionData | undefined; }I cannot explain why this works, but it does. TS is black magic some times.
Reacted by JavidIt means that property session has to be defined, while with question mark operator it doesn't have to
@atassis Of course I know that. I wrote the type that narrows down
ctx.has. See the thread leading to my reply.- locked as resolved and limited conversation to collaborators
on Sep 30, 2023
The


messageproperty ofctxhas a lot of missing type definitions which don't follow the official ones.The line of code causing the error was this one:
bot.on("message", (ctx) => ctx.reply(ctx.message.text));The bot should reply with the text you send to him.