Skip to content

Conversation

KnorpelSenf
Copy link
Collaborator

@KnorpelSenf KnorpelSenf commented Nov 27, 2020

Description

typegramallows to narrow down certain types (most notably Update and Message) to its different subtypes. However, telegraf does not use this feature yet (reason).
We therefore have to check if message.text is present even in the case where we register the middleware only for text events and hence know in advance that it is always truthy.

This PR adds a number of new types to the library that allow for type inference on the context object in middleware functions. As a result, a context object is correctly assumed to provide a message property when it is the argument of a middleware function that is being passed to bot.on('message'). The same is true for subtypes of messages.

Furthermore, it is possible to provide a list of update types and message subtypes. The type inference will then guarantee that all of the relevant properties are available, but optional. Thus, we get autocompletion for the properties that make sense, while still having to check explicitly which of them is actually present. Even here it is possible to narrow down the type of update, i.e. when registering for ['callback_query', 'text'] and then checking if either of the update types is present, the other option will be discarded. (Callback queries and text messages are mutually exclusive in an update.)

Registering middleware on forward correctly assures that forward_date is present, even though the property forward will never exist on the message. In other words, the type mapping of telegraf that happens under the hood is regarded.

Even though this PR is good as it is, there is still one open question that we may want to discuss before merging. telegraf supports switching on channelMode. When activated, all channel posts are handled as messages. What happens in this case? It is technically feasible to make message optional every time and also provide channel_post as an optional property. In the current state, channel_post is said to be unavailable when registering middleware for text while message is guaranteed to be present. This happens regardless of channelMode (which is obviously wrong). It may be possible to do this by adding another type variable to Composer.

Type of change

Only changes type annotations. Might require elaborations on how to use this project with TypeScript, depending on how we decide on the aforementioned open question.

How Has This Been Tested?

npm run build
npm run lint
npm t

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation (perhaps we need docs about this)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works (we need more examples or example bots, confer e.g. Add an example for Stage and Scene with a custom context #1139)
  • New and existing unit tests pass locally with my changes

Copy link
Member

@wojpawlik wojpawlik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive.

I'm not sure how this typechecks without updating corresponding static methods, but I expect inference to break when you do.

Minor thing: ideally, for example in composer.on('message', ctx => ...), ctx.editedMessage and others should be undefined (currently they're left as optional).

telegraf supports switching on channelMode. [...] It is technically feasible to make message optional every time and also provide channel_post as an optional property.

That's what I did in my failed attempt.

@KnorpelSenf KnorpelSenf marked this pull request as ready for review November 29, 2020 00:13
@KnorpelSenf
Copy link
Collaborator Author

I'm not sure how this typechecks without updating corresponding static methods, but I expect inference to break when you do.

Updating these methods is both required and not a problem.

Note that there is one difference to a previous approach: it is not possible to infer from previous middleware arguments the types of the next. This is available for compose, but other methods use compositional inference rather than inferring the types from the usage. This solves the problem we had earlier and it works very well.

Minor thing: ideally, for example in composer.on('message', ctx => ...), ctx.editedMessage and others should be undefined (currently they're left as optional).

Good idea! I would not have thought about it. I added that now.

@KnorpelSenf
Copy link
Collaborator Author

KnorpelSenf commented Nov 29, 2020

I do have to add that we should write more documentation about how to use telegraf with TypeScript. Given the size of this PR, however, it might make sense to do this in the next iteration.

@wojpawlik feel free to play around with the example bots, the types are working well there IMO. Looking forward to hearing your opinion about it.

We also need to decide on the problem with channelMode.

src/composer.ts Outdated
C extends Context,
T extends tt.UpdateType | tt.MessageSubType
> = C &
// TODO: support subtypes for: callback query, channel post, maybe more?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support type inference in even more cases, not just for subtypes of message?

Copy link
Collaborator Author

@KnorpelSenf KnorpelSenf Nov 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, when using a MessageSubType, we infer subtypes for the message property only, even though it could come from a different update type.

Take this code:

bot.on('text', ctx => {
  const text = ctx.message.text // works
})

We correctly infer ctx.message.text: string as well as ctx.update.message.text: string.

However, mount not only checks message texts, but also the content of all update types, including callback queries, inline queries, channel posts, etc.

Take this code:

bot.on('channel_chat_created', ctx => {
  const chat = ctx.channelPost.chat // error
})

We infer both ctx.message.channel_chat_created and ctx.update.message.channel_chat_created as true and ctx.channelPost as undefined, all of which is incorrect.

I'm suggesting that we should also do a similar thing for other subtypes that we provide. It may make sense to do this generically, but I believe that this will create too complex types again, so we may have to come up with something smarter.

Note: the TODO comment in the code is not quite correct about callback queries as neither 'data' nor 'game_short_name' are valid message subtypes, so callback queries should in fact not be the issue here. It's about channel posts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, it basically boils down to supporting channel mode also in this place.

@wojpawlik wojpawlik added this to the v4 milestone Nov 29, 2020
@wojpawlik wojpawlik mentioned this pull request Nov 29, 2020
26 tasks
@wojpawlik
Copy link
Member

Tbh., I'd suggest to remove channelMode and replace subtypes to be like edited_message:text in next PR.

@KnorpelSenf
Copy link
Collaborator Author

KnorpelSenf commented Nov 29, 2020

Tbh., I'd suggest to remove channelMode and replace subtypes to be like edited_message:text in next PR.

Sounds like a perfect fit for template literal types.

I like the idea actually. This should make it much easier to add types and to explain the lib. I remember well how confused I was in the beginning that I could do both on('message') and on('text')and it meant very different things.

I suggest in turn that we leave this PR as it is, get a review done, and then open a third PR that does the rest before releasing 4.0.

@wojpawlik
Copy link
Member

Actually, the edited_message:text format I suggested doesn't allow to express more complex queries like "forwarded text message". Ideally, in 5.0 optional could accept a type guard, and then optional(is.message(is.forwarded, is.text), handler). Not sure what's the best for now tho.

@KnorpelSenf
Copy link
Collaborator Author

I see the point that reworking how to listen to update types should be done thoroughly. This opens two questions:

  1. Given that we won't change the behaviour of bot.on for now, how are we going to go about channelMode then? I looked into adding types for it, but that would require a second or third type variable for all methods in Composer. I have no idea whether TypeScript could handle this (think of all the unions), and I won't try this before we deem it necessary.

  2. Do we have a roadmap to what needs to be done before 4.0? I feel like the v4 milestone is updated whenever we feel like it, flexibly adding and removing things as we like. I'd like to somehow put on record which parts we want to include in 4.0 and which parts we postpone.

@wojpawlik
Copy link
Member

I put every breaking change that comes up into the milestone.

@KnorpelSenf KnorpelSenf mentioned this pull request Nov 30, 2020
8 tasks
@wojpawlik wojpawlik marked this pull request as draft December 1, 2020 10:22
@KnorpelSenf
Copy link
Collaborator Author

One possible solution here is to exclude channelPost from being inferred as undefined. That way, users of channelMode retain access to the property, while others probably won't care.

After applying this change, the types are still a bit incorrect for channelMode = true in the way that message should in fact be optional while it is inferred as required, and they are a bit incorrect for channelMode = false in the way that channelPost should be undefined while it is inferred as undefined | tt.Message. Either way, we're getting pretty close to being completely correct, while saving us a lot of work. This PR would be feature complete (after this small change).

Copy link
Member

@wojpawlik wojpawlik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last question, what do you have against Parameters? 😄

@KnorpelSenf
Copy link
Collaborator Author

Nothing really. It makes things less readable and in some cases more maintainable. I try to avoid them for complex type signatures. If I do advanced stuff, I don't want things to be more implicit than necessary. Why?

@wojpawlik
Copy link
Member

DRY?

@KnorpelSenf
Copy link
Collaborator Author

Yes. That's what I mean with more maintainable.

It's useful and I like it for what it can do, but it comes at a cost and sometimes I think it's better not to pay that.

@KnorpelSenf
Copy link
Collaborator Author

KnorpelSenf commented Dec 2, 2020

But does the PR work? Do the voodoo magic types do their job? I'm looking forward to your optional thingy 😄
Our guard or whatever it will be

Copy link
Member

@wojpawlik wojpawlik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry too much about other people verifying the new types, I'll try to simplify them right after this is merged :)

In the meantime, I'll work on type guard functions to PR along with Composer::guard after this is merged.

@wojpawlik
Copy link
Member

Anything else, or can I merge?

@KnorpelSenf
Copy link
Collaborator Author

Anything else, or can I merge?

You can merge.

@wojpawlik wojpawlik marked this pull request as ready for review December 5, 2020 17:42
@wojpawlik wojpawlik merged commit 3c4c935 into develop Dec 5, 2020
@wojpawlik wojpawlik deleted the leverage-unionization branch December 5, 2020 17:42
@wojpawlik
Copy link
Member

Now it's my time to shine

@KnorpelSenf
Copy link
Collaborator Author

KnorpelSenf commented Dec 5, 2020

Looking forward to it! Also, you've already been shining pretty bright so far with very good reviews. (Not to forget the rest.)

@wojpawlik wojpawlik mentioned this pull request Dec 6, 2020
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants