telegraf: Missing type definitions on ctx.message

The message property of ctx has a lot of missing type definitions which don’t follow the official ones. Missing properties Missing text property

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.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 6
  • Comments: 25 (12 by maintainers)

Commits related to this issue

Most upvoted comments

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.

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:

  1. Use bot.on('text', (ctx) => ...). It’s that simple.
  2. Narrow manually: if (!('text' in ctx.message)) return next() (messy when ctx.message might be undefined).
  3. Manually use a type guard (not included): if (!has.text(ctx.message)) return next().
  4. Pass a type guard to Composer.guard.
  5. deunionize(ctx.message).text.

What could Telegraf 5 do?

  1. Simply make each Message property 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!))
    
  2. Add _type discriminator property. One needs to know to check it. Needs extra code that breaks with API updates.
  3. deunionize by default. After narrowing manually, suggestions are bloated with multiple ?: undefined properties.

MatchedContext<Context<Update>, "text"> is equivalent to NarrowedContext<Context, MountMap["text"]>, which is roughly equivalent to Context<MountMap["text"]>.

Context and NarrowedContext are exported, MountMap is not. Should I export it in 4.4? Is there a better name for it?

For now, I suggest copying it.

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.on and ctx.has support 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(), and callbackQuery("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

@amlxv

import type { Update, Message } from "telegraf/types";

const handleRequest = async (ctx: Context<Update.MessageUpdate<Message.TextMessage>>) {
	// ...
}

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 is MatchedContext<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 using Context<Update> as type for ctx the information about the MatchedContext type “text” gets lost.

@atassis For your specific case, the correct way to handle it is:

bot.on("message", (ctx, next) => {
	if ("contact" in ctx.message) {
		console.log(ctx.message.contact);
	}
});

TS is (not) telling you that all message updates are not contact messages. When you filter using this if-check, TS knows exactly what kind of message update you’re dealing with.

Hello, can someone explain (or add more details to examples) how to use WizardScene and read user input for specific step. Because a lot of examples uses message.text (for example https://stackoverflow.com/questions/55749437/stage-enter-doesnt-start-the-wizard) but it does not work with typescript. Because if you just started to learn telegraf and without googling this issue #1388 - this is really tricky to understand.

At this moment i found only one simple solution, but i am not sure that is correct. Thank you.

async (ctx) => {
    await ctx.reply('Step 3');

    if (ctx.message && 'text' in ctx.message) {
        console.log(ctx.message.text);
        return ctx.wizard.next();
    } else {
        return ctx.reply('Please input your details');
    }
},

I don’t have a good idea for a name, but thanks for the clarification. Exporting it for usage in interfaces would be amazing. Thank you very much.

@djeks922 I was able to recreate your problem. Make session non-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.

I was using the spreaded variable in the condition and that didn’t specify the exact type somehow, “god works in mysterious ways”. And couldn’t find the typings in the source code, now I see there is a Typegram being used under the hood, so thats where typings come from