-
Notifications
You must be signed in to change notification settings - Fork 395
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
Improve types for message events and all subtypes #709
Improve types for message events and all subtypes #709
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a big fan of this refactor. Much easier to read and follow
type: Type; | ||
} | ||
|
||
/* ------- 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
userId?: string; | ||
conversationId?: string; | ||
isEnterpriseInstall?: boolean; | ||
isEnterpriseInstall: IsEnterpriseInstall; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()
).
@@ -979,10 +943,10 @@ function defaultErrorHandler(logger: Logger): ErrorHandler { | |||
}; | |||
} | |||
|
|||
function singleTeamAuthorization( | |||
function singleAuthorization( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
||
/** Org Authorize */ | ||
private orgAuthorize!: Authorize; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my approval for the event payload related parts
| MessageRepliedEvent | ||
| ThreadBroadcastMessageEvent; | ||
|
||
export interface GenericMessageEvent { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's create a new issue for it!
conversationId: channelId, | ||
isEnterpriseInstall: false, | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this check should be moved below line 850 check. Why would body.team.enterprise_id
take priority over body.enterprise.id
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
@@ -994,8 +958,8 @@ function singleTeamAuthorization( | |||
}; | |||
}); | |||
|
|||
return async () => { | |||
return { botToken: authorization.botToken, ...(await identifiers) }; | |||
return async ({ isEnterpriseInstall }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably just a brain fart, but where is this isEnterpriseInstall
coming from here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
SlackEvent
union included a type calledMessageEvent
which represents all Events API payloads withtype:'message'
. However, the union didn't include the message event subtypes (such assubtype='bot_message'
).With this change,
MessageEvent
is itself a union of all the subtypes. That union is still part of the overallSlackEvent
union. 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
SlackEvent
union. 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/bolt
are 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
MessageEvent
is 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 themessage
argument insideapp.message()
listeners to theGenericMessageEvent
type. 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 ofmessage
and safely use properties like below:Requirements (place an
x
in each[ ]
)