Improve types for message events and all subtypes#709
Improve types for message events and all subtypes#709aoberoi merged 12 commits intoslackapi:mainfrom
Conversation
Moved all the subtypes into a new file to keep them more organized. Wrote a test file that should theoretically pass when this issue is fixed. Does not yet pass TS compiler, more changes to come.
Once we removed StringIndexed from SlackEvent types, many property lookups became typechecking errors. I borrowed this refactor mostly from a WIP PR to improve our style/linting in a proactive effort to redue conflicts when that PR lands. The only changes I needed to add were the Org Apps changes that had landed on main. slackapi#669 In a few cases, the event types had other issues that became apparent only after removing the aggressive casting we had before. Those are fixed. It's now evident that there's more type related problems with respect to the Authorize, its related types, especially the OrgApps variants. I'll try to fix these next. It's also not clear if buildSource needs the orgInstall argument. Will seek advice in code review for that.
By making these two types generic, we can resolve the typing issue in buildSource and a couple other places. This code still doesn't compile. May need to update singleTeamAuthorization. Getting close.
In both App.ts and builtin.ts, issues were resolve simply by refactoring to an equivalent form that is more type-safe. In App.ts specifically, processEvent() was refactored to reduce some code repetition as well. In helpers.ts this actually fixes bugs. The getTypeAndConversation() would have failed conversation lookup for the following event types: pin_added, pin_removed, call_rejected, channel_created, channel_rename, group_rename, im_created.
Codecov Report
@@ Coverage Diff @@
## main #709 +/- ##
==========================================
+ Coverage 82.32% 82.85% +0.53%
==========================================
Files 8 8
Lines 758 770 +12
Branches 250 230 -20
==========================================
+ Hits 624 638 +14
- Misses 78 87 +9
+ Partials 56 45 -11
Continue to review full report at Codecov.
|
| conversationId: channelId, | ||
| isEnterpriseInstall: false, | ||
| }; | ||
|
|
There was a problem hiding this comment.
This refactor is mostly borrowed from #669, so that when it eventually lands, there's less chance of a merge conflict.
I found all the Org Apps related changes on main and layered them into this part of code from the linting PR.
Prettier undid some of my style related changes, but these will likely be automatically fixed again once the linting PR lands.
There was a problem hiding this comment.
I'm a big fan of this refactor. Much easier to read and follow
|
|
||
| /* ------- TODO: Generate these interfaces ------- */ | ||
|
|
||
| // TODO: why are these all StringIndexed? who does that really help when going more than one level deep means you have |
There was a problem hiding this comment.
Once I removed StringIndexed as a base from all the event interfaces, many unsafe property accesses in our own codebase became type-checking errors. Most of refactoring in the rest of this PR is simply an attempt to address those errors.
| conversationId?: string; | ||
| isEnterpriseInstall?: boolean; | ||
| isEnterpriseInstall: IsEnterpriseInstall; | ||
| } |
There was a problem hiding this comment.
This is likely the most meaningful refactor. I've done something similar here to what I did in node-slack-sdk - consolidated the types that were very similar but just varied on IsEnterpriseInstall by making the types generic.
This allowed me to safely reduce code repetition within App.processEvent() and singleTeamAuthorization() (now singleAuthorization()).
| } | ||
|
|
||
| function singleTeamAuthorization( | ||
| function singleAuthorization( |
There was a problem hiding this comment.
The rename is because this function addresses authorization on a single team OR a single enterprise. I considered having two separate functions for those purposes, but realized the body would look mostly identical except for the value of isEnterpriseInstall in the return value. That could easily be consolidated because isEnterpriseInstall is actually passed to the Authorize function as a property of source anyway.
| const { event } = body as SlackEventMiddlewareArgs<string>['body']; | ||
|
|
||
| // Find conversationId | ||
| const conversationId: string | undefined = (() => { |
There was a problem hiding this comment.
This code actually got a bit longer because it fixes some actual bugs (not just a straight refactor). Previously, events of types pin_added, pin_removed, call_rejected, channel_created, channel_rename, group_rename, and im_created would not have a conversationId in the return value because the conditional checks were incomplete.
| private authorize!: Authorize<false>; | ||
|
|
||
| /** Org Authorize */ | ||
| private orgAuthorize!: Authorize; |
There was a problem hiding this comment.
This was technically wrong previously. The Authorize type describes a function which non-optionally took a teamId.
| }; | ||
| } | ||
|
|
||
| function isBodyWithTypeEnterpriseInstall(body: AnyMiddlewareArgs['body'], type: IncomingEventType): boolean { |
There was a problem hiding this comment.
New helper function so the calling code is more readable.
|
Regarding the removal of It seems this pull request consists of the following changes. And, if I understand correctly, 3 & 4 changes do not rely on 1 & 2.
I haven't checked the details for authorize changes and a bug fix in |
seratch
left a comment
There was a problem hiding this comment.
my approval for the event payload related parts
| | MessageRepliedEvent | ||
| | ThreadBroadcastMessageEvent; | ||
|
|
||
| export interface GenericMessageEvent { |
There was a problem hiding this comment.
The type definition seems to be missing some fields such as team and channel_type. You can check the Java SDK's type definition to learn them. https://github.com/slackapi/java-slack-sdk/tree/v1.4.0/slack-api-model/src/main/java/com/slack/api/model/event
There was a problem hiding this comment.
Great catch! Should I create another issue for this so it can be addressed in a separate PR? This PR was meant to solve the structural issue with the union types, but the issue you've pointed out is independent of that.
There was a problem hiding this comment.
Let's create a new issue for it!
| conversationId: channelId, | ||
| isEnterpriseInstall: false, | ||
| }; | ||
|
|
There was a problem hiding this comment.
I'm a big fan of this refactor. Much easier to read and follow
|
|
||
| // When the app is installed using org-wide deployment, team property will be null | ||
| if (bodyAsActionOrOptionsOrViewActionOrShortcut.team !== null) { | ||
| return bodyAsActionOrOptionsOrViewActionOrShortcut.team.enterprise_id; |
There was a problem hiding this comment.
I wonder if this check should be moved below line 850 check. Why would body.team.enterprise_id take priority over body.enterprise.id?
|
|
||
| return async () => { | ||
| return { botToken: authorization.botToken, ...(await identifiers) }; | ||
| return async ({ isEnterpriseInstall }) => { |
There was a problem hiding this comment.
probably just a brain fart, but where is this isEnterpriseInstall coming from here?
There was a problem hiding this comment.
Yeah higher order functions can be mind-bending for me too. singleAuthorization() returns a function, which as the type Authorize. So this expression is of type Authorize. Which means the argument is of type AuthorizeSourceData. So this isEnterpriseInstall is the property of AuthorizeSourceData, defined on line 94.
As for the actual value, its from the return value of buildSource().
Co-authored-by: Steve Gill <stevengill97@gmail.com>
@seratch I initially started this PR to only work on 3 & 4, but as soon as those were fixed (specifically removing I agree that keeping PRs small and focused makes them easier to review, and easier to land when there's several changes worked on by different people. In order to do that here, I think I have to first land the changes related to 1 & 2 and then land the changes related to 3 & 4 (since the changes in 3 & 4 will not build otherwise). This PR would represent the latter (that's already the portion you reviewed). I'll give that a try. |
|
Instead of spending time breaking this into two, I suggest we move forward with merging this PR! It is looking pretty solid to me! |
Co-authored-by: Steve Gill <stevengill97@gmail.com>
Summary
Previously, the
SlackEventunion included a type calledMessageEventwhich represents all Events API payloads withtype:'message'. However, the union didn't include the message event subtypes (such assubtype='bot_message').With this change,
MessageEventis itself a union of all the subtypes. That union is still part of the overallSlackEventunion. This means that both theapp.event()andapp.message()methods are now aware of message subtypes.Fixes #311
Another related change is also in this PR: the index type has been removed from all events in the
SlackEventunion. This means you can no longer safely read any property name from any event and treat the value as anany.NOTE: This will cause new type checking errors where there wasn't one before in some apps. Those errors will be legitimate (unsafe accesses to event properties), but these apps may have been working fine until now and builds could be broken. This is actually a feature of using TypeScript, this isn't considered a breaking change.
If you're reading this because your build broke and you're looking for a quick fix, it may be helpful to know two things:
If you know that the property you're trying to access is available on the event, here is how you should approach it. First, maybe the types in
@slack/boltare out of date or wrong - consider creating an issue in the repo. Second, you can use a cast to say "I know better than Bolt's types" and immediately unblock yourself.The type that was previously called
MessageEventis now calledGenericMessageEvent. This new type is meant to be used to represent Events API messages of type'message', but which do not have a'subtype'. You can likely get your build working again by intentionally casting themessageargument insideapp.message()listeners to theGenericMessageEventtype. But before you do this, consider taking any errors seriously as you may actually have an unsafe access to a property. You can use a conditional check to narrow the type ofmessageand safely use properties like below:Requirements (place an
xin each[ ])