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

Fix #904 by improving app.message listener's TS compatibility while bringing breaking changes #1801

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

seratch
Copy link
Member

@seratch seratch commented Apr 11, 2023

Summary

As mentioned in #904, implementing app.message listeners in TypeScript can be confusing as the payload/message objects are union types, so developers must have if/else statements to determine the message payload. In most cases, developers just want to handle newly posted messages in the listener. Having to add if (!payload.subtype || payload.subtype === 'bot_message' || payload.subtype === 'file_share' || payload.subtype === 'thread_broadcast') { ... } in all app.message listeners is not a great developer experience.

app.message(async ({ payload }) => {
  // This compiles! But you no longer receive message_changed etc.
  assert.isNotNull(payload.channel);
  assert.isNotNull(payload.ts);
  assert.isNotNull(payload.text);
  assert.isNotNull(payload.blocks);
  assert.isNotNull(payload.attachments);
});

I propose improving the app.message listener types in this pull request. Most developers will be happy with the change because, with the changes in this PR, developers no longer need to have the if/else blocks. One downside of this approach is that other subtype patterns, such as message_changed, won't be delivered to app.message listeners anymore. This behavioral modification can be a breaking change to some of the existing apps. For those apps, I've added app.allMessageSubtypes listener, which is fully compatible with the current app.message listener's behavior.

app.allMessageSubtypes(async ({ payload }) => {
  // You can still receive all the subtype requests in this listener
  if ((!payload.subtype ||
    payload.subtype === 'bot_message' ||
    payload.subtype === 'file_share' ||
    payload.subtype === 'thread_broadcast')) {
    // This compiles
    assert.isNotNull(payload.channel);
    assert.isNotNull(payload.ts);
    assert.isNotNull(payload.text);
    assert.isNotNull(payload.blocks);
    assert.isNotNull(payload.attachments);
  }
});

I would love to hear what other maintainers and community members think abou this proposal! If it looks good to many, we may merge the change in a future major or minor release. In theory, we should have this change in the next major but it may take long. Is it worth considering to have it in a minor release? Or, should we plan releasing next major soon?

Requirements (place an x in each [ ])

@seratch seratch added bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented semver:major semver:minor labels Apr 11, 2023
pattern: string | RegExp,
): Middleware<SlackEventMiddlewareArgs<'message' | 'app_mention'>> {
Copy link
Member Author

Choose a reason for hiding this comment

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

having app_mention here had been false as a preceding built-in middleware matchEventType('message') does not accept app_mention. Moreover, it should not accept two types of events. So, I've removed it.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK so this change implies that matchMessage should only ever be used with a message event - might be useful to clarify that in the JSDoc, as the JSDoc in its current form implies this can be used for any event that contains a message.

@seratch seratch marked this pull request as draft April 11, 2023 09:26
@seratch
Copy link
Member Author

seratch commented Apr 11, 2023

Coverted to draft as it seems that this PR is not yet complete.

@seratch seratch marked this pull request as ready for review April 11, 2023 09:42
@seratch
Copy link
Member Author

seratch commented Apr 11, 2023

Resolved the issues with app.event listeners

@codecov
Copy link

codecov bot commented Apr 11, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 82.07%. Comparing base (449eced) to head (22b5179).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1801      +/-   ##
==========================================
+ Coverage   81.97%   82.07%   +0.10%     
==========================================
  Files          18       18              
  Lines        1531     1540       +9     
  Branches      440      443       +3     
==========================================
+ Hits         1255     1264       +9     
  Misses        178      178              
  Partials       98       98              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

type MessageMiddlewareArgs = SlackEventMiddlewareArgs<'message'> & MiddlewareCommonArgs;
type MessageMiddlewareArgs = SlackEventMiddlewareArgs<
'message',
undefined | 'bot_message' | 'file_share' | 'thread_broadcast'
Copy link
Member Author

Choose a reason for hiding this comment

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

this aligns with bolt-python

@zimeg
Copy link
Member

zimeg commented Apr 17, 2023

Wow, this is sweet! These changes seem really helpful for improving the experience with TypeScript, particularly with the first Hello, <@${message.user}> example. I'm a fan of these changes and agree that they improve a very common development path, thank you for putting this together! 👏 🎉

The app.allMessageSubtypes listener already seems like a useful way to access messages with a subtype, but I'm wondering if specifying the subtype as a listener constraint for app.message might provide a more consistent experience, where these subtypes are more "opt-in"? I'm unsure of the most common use cases here, but I would hope that this could also improve typing when using specific subtypes (assuming that typing can be inferred from this constraint parameter).

But regardless of that last point (it's only a suggestion that you can totally ignore!) I think this is a great proposal and would love to see it in an upcoming release! I agree that because this can be a breaking change, it may be better for a major release, but also understand releasing it in a minor version to bring these improvements to the near-term. The minor release would just require good communication about any breakages. However, if there are more breaking changes on the horizon, then planning for a major release soon would make more sense to me.

@seratch seratch added this to the 4.0.0 milestone May 18, 2023
@seratch seratch self-assigned this May 18, 2023
Copy link
Contributor

@filmaj filmaj left a comment

Choose a reason for hiding this comment

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

Looks good to me! Too bad about the breaking change but I am not sure if there is another option.

I wonder if it would be helpful to meet or collaborate to understand what other (if any) pending breaking changes we could address?

> = Middleware<SlackEventMiddlewareArgs<'message'>, CustomContext>;
> = Middleware<SlackEventMiddlewareArgs<
'message',
undefined | 'bot_message' | 'file_share' | 'thread_broadcast'
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it is worth extracting this union into its own type?
What does this combination represent? The string literals seem to be message subtypes, but undefined in this context represents what? Generic message event? Message event with no subtype?
Just trying to better understand the intention.

): void {
const messageMiddleware = patternsOrMiddleware.map((patternOrMiddleware) => {
if (typeof patternOrMiddleware === 'string' || util.types.isRegExp(patternOrMiddleware)) {
return matchMessage<undefined | 'bot_message' | 'file_share' | 'thread_broadcast'>(patternOrMiddleware, true);
Copy link
Contributor

Choose a reason for hiding this comment

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

If my comment above is followed (to extract this union into its own type), we can then re-use it here.

MiddlewareCustomContext extends StringIndexed = StringIndexed,
>(
filter: AllMessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>,
pattern: string | RegExp,
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to clarify, filter in these arguments means a middleware filter, whereas pattern is a message text pattern match / filter?

filter: AllMessageEventMiddleware,
...listeners: AllMessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>[]
): void;
public allMessageSubtypes<
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this overload signature the same as the implementation signature that follows, or am I misreading this?

pattern: string | RegExp,
): Middleware<SlackEventMiddlewareArgs<'message' | 'app_mention'>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

OK so this change implies that matchMessage should only ever be used with a message event - might be useful to clarify that in the JSDoc, as the JSDoc in its current form implies this can be used for any event that contains a message.

expectType<MessagePostedEvent>(message);

message.channel; // the property access should compile
message.user; // the property access should compile
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

it('should provide better typed payloads', async () => {
app.message(async ({ payload }) => {
// verify it compiles
assert.isNotNull(payload.channel);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just an idea: many of the assert methods take a type parameter that then ensures the type of the argument provided to assert is of the provided type. This way perhaps it is clearer the intention of the test ("verify it compiles").

Suggested change
assert.isNotNull(payload.channel);
assert.isNotNull<string>(payload.channel);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented semver:major
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants