Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing type definitions on ctx.message #1388

Closed
markkarrica opened this issue Mar 5, 2021 · 25 comments · Fixed by #1698
Closed

Missing type definitions on ctx.message #1388

markkarrica opened this issue Mar 5, 2021 · 25 comments · Fixed by #1698

Comments

@markkarrica
Copy link

markkarrica commented Mar 5, 2021

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.

@wojpawlik
Copy link
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.

@equt
Copy link

equt commented Mar 6, 2021

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.

@wojpawlik
Copy link
Member

@equt as is unsafe.

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

@wojpawlik wojpawlik reopened this Mar 27, 2021
@enyineer
Copy link

enyineer commented Mar 27, 2021

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.

@wojpawlik
Copy link
Member

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.

@enyineer
Copy link

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.

@VeXell
Copy link

VeXell commented Jun 23, 2021

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');
    }
},

@bokssssss
Copy link

okay thanks the information

@MKRhere MKRhere removed the wontfix label Dec 7, 2021
@Edward-Fedoruk
Copy link

Any updates on this issue?

@atassis
Copy link
Contributor

atassis commented Jun 30, 2022

I got no idea thats going on with my issue, even after seeing a relation between mine and this issue. Thats a plan of maintainers to deal with a lack of typings. I tried to solve that properly somehow in my case, but didnt find any good way by myself
@wojpawlik ?

@MKRhere
Copy link
Member

MKRhere commented Jul 1, 2022

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

@atassis
Copy link
Contributor

atassis commented Jul 1, 2022

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

@MKRhere MKRhere self-assigned this Sep 18, 2022
@wojpawlik wojpawlik linked a pull request Sep 27, 2022 that will close this issue
4 tasks
@MKRhere
Copy link
Member

MKRhere commented Nov 25, 2022

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

@quinn
Copy link

quinn commented Dec 12, 2022

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

})

the correct typing of ctx has so far eluded me

@amlxv
Copy link

amlxv commented Mar 17, 2023

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)
};

@MKRhere
Copy link
Member

MKRhere commented Mar 17, 2023

@amlxv

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

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

@djeks922
Copy link

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

@MKRhere
Copy link
Member

MKRhere commented Mar 30, 2023

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

@djeks922
Copy link

// 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>>>'

@djeks922
Copy link

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

@djeks922
Copy link

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

@djeks922
Copy link

djeks922 commented Mar 30, 2023

'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

@MKRhere
Copy link
Member

MKRhere commented Mar 31, 2023

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

@atassis
Copy link
Contributor

atassis commented Mar 31, 2023

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

@MKRhere
Copy link
Member

MKRhere commented Mar 31, 2023

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.