Skip to content

Missing type definitions on ctx.message #1388

Description

@markkarrica

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.

Activity

  1. pinned this issue on Mar 5, 2021
  2. wojpawlik commented on Mar 5, 2021

    @wojpawlik
    Member

    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.
  3. equt commented on Mar 6, 2021

    @equt

    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.

  4. wojpawlik commented on Mar 27, 2021

    @wojpawlik
    Member

    @equt as is unsafe.

    I think I'll Deunionize Context getters in Telegraf 5 (option 8).

  5. enyineer commented on Mar 27, 2021

    @enyineer

    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.

  6. 16 remaining items

  7. MKRhere commented on Nov 25, 2022

    @MKRhere
    Member

    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

  8. quinn commented on Dec 12, 2022

    @quinn
    would it be possible to show a typed example of:
    bot.on('message', (ctx) => {
    
    })

    the correct typing of ctx has so far eluded me

  9. amlxv commented on Mar 17, 2023

    @amlxv

    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 handleRequest parameter?

    bot.on(message('text'), async (ctx) => {
      await handleRequest(ctx);
    });
    
    const handleRequest = async (ctx: ?) => {
        console.log(ctx.message?.text)
    };
  10. MKRhere commented on Mar 17, 2023

    @MKRhere
    Member

    @amlxv

    import type { Update, Message } from "telegraf/types";
    
    const handleRequest = async (ctx: Context<Update.MessageUpdate<Message.TextMessage>>) {
    	// ...
    }
  11. djeks922 commented on Mar 30, 2023

    @djeks922

    for custom Context, lets say MyContext where custom session present
    when we check for

    if(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>>

  12. MKRhere commented on Mar 30, 2023

    @MKRhere
    Member

    @djeks922 I cannot reproduce this. Can you show more surrounding code?

  13. djeks922 commented on Mar 30, 2023

    @djeks922
    // 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>>>'
    
  14. djeks922 commented on Mar 30, 2023

    @djeks922
    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

  15. djeks922 commented on Mar 30, 2023

    @djeks922

    I'm not good at typescript so, maybe is there some option to pass MyContext as generic T to .has or similar way?

  16. djeks922 commented on Mar 30, 2023

    @djeks922

    'NarrowedContext<Context, MessageUpdate<Record<"voice", {}> & VoiceMessage & AddOptionalKeys>>'
    Because has changes MyContext to Context when narrowing, ignoring the parent object type, in our case MyContext

    Thanks beforehand, I hope I was able to explain the problem

  17. MKRhere commented on Mar 31, 2023

    @MKRhere
    Member

    @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.

  18. atassis commented on Mar 31, 2023

    @atassis
    Contributor

    It means that property session has to be defined, while with question mark operator it doesn't have to

  19. MKRhere commented on Mar 31, 2023

    @MKRhere
    Member

    @atassis Of course I know that. I wrote the type that narrows down ctx.has. See the thread leading to my reply.

  20. locked as resolved and limited conversation to collaborators on Sep 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions