bolt-js
bolt-js copied to clipboard
app.message payload arg compatibility in TypeScript
The message
argument in app.message
listeners does not provide sufficient properties in TypeScript.
Property 'user' does not exist on type 'KnownEventFromType<"message">'. Property 'user' does not exist on type 'MessageChangedEvent'.ts(2339)
A workaround is to cast the message
value by (message as GenericMessageEvent).user
but needless to say, this is not great.
I ran into this today also. Trying to use the official example from https://slack.dev/bolt-js/concepts
// This will match any message that contains π
app.message(':wave:', async ({ message, say }) => {
await say(`Hello, <@${message.user}>`);
});
and immediately getting a ts compile error. This is not a great first-time experience. Not even sure how to get it working.
Originally posted by @memark in https://github.com/slackapi/bolt-js/issues/826#issuecomment-830565064
Update: https://github.com/slackapi/bolt-js/pull/871 can be a solution for this but I'm still exploring a better way to resolve this issue. One concern I have about my changes at #871 would be the type parameter could be confusing as it does not work as a constraint.
π any news here? It's been 4 months since I found this page And it still didn't move at all.
If I start a new project using the given sample project, will I have troubles?
Is it recommended to not use Typescript with Bolt?
My advice: don't bother trying to use Bolt with Typescript at this point. Perhaps one day slack will genuinely prioritize Typescript.
update your tsconfig.json to have
"esModuleInterop": true
.
It helped me.
The message.subtype
property is used as a discriminator in the message event type definition. The regular message event (the one which is expected to be received in the example) has no subtype
property.
In my opinion, the correct way to make the example work is:
// This will match any message that contains π
app.message(':wave:', async ({ message, say }) => {
if (!message.subtype) {
await say(`Hello, <@${message.user}>`);
}
});
It's a pity to see that after a year since this issue has been created we still don't have a ts support of the very basic example in the README :-/
"@slack/bolt": "^3.12.1",
"typescript": "^4.7.4"
This one works for me :smile:
if (
message.subtype !== "message_deleted" &&
message.subtype !== "message_replied" &&
message.subtype !== "message_changed"
) {
await say(`Hello, <@${message.user}>`);
}
As you can see in message-events.d.ts
,
...
export interface MessageChangedEvent {
type: 'message';
subtype: 'message_changed';
event_ts: string;
hidden: true;
channel: string;
channel_type: channelTypes;
ts: string;
message: MessageEvent;
previous_message: MessageEvent;
}
export interface MessageDeletedEvent {
type: 'message';
subtype: 'message_deleted';
event_ts: string;
hidden: true;
channel: string;
channel_type: channelTypes;
ts: string;
deleted_ts: string;
previous_message: MessageEvent;
}
export interface MessageRepliedEvent {
type: 'message';
subtype: 'message_replied';
event_ts: string;
hidden: true;
channel: string;
channel_type: channelTypes;
ts: string;
message: MessageEvent & {
thread_ts: string;
reply_count: number;
replies: MessageEvent[];
};
}
...
these three interfaces don't have a user
property :smiley:
Any updates on proper support or updating the documentation so it works as described using TS?
I just did this casting to solve the problem in my case: message as {user:string}
app.message('hallo', async ({message, say}) => {
await say(`Hallo zurΓΌck <@${(message as {user:string} /* narrow it to what i want to access! is it called "narrowing?"*/).user}>`)
/** then print the message object to make sure it is still unchanged */
console.log(JSON.stringify(message))
});
Just ran into the complete lack of proper TypeScript support by BoltJS π’
As of November 2022 Slack has a market cap of $26.51 Billion
My workaround is to put all the Bolt logic in .js
files, and make custom types for Block Kit.
@seratch what's the state-of-things with proper TypeScript support? Seems we got a lot of hopeful promises ~2 years ago (https://github.com/slackapi/bolt-js/issues/826), but the issue was closed out quickly and ever since then, other TS-related issues just get hacky workarounds proposed as accepted solutions π
I am going to give this bolt library a try.
if I encounter any type errors and want to propose a pull request to assist with improving typescript support would that be useful and welcome?
this is my first time building a bot and it's not a great experience. would love to see this prioritized. in the meantime, discord seems to have typescript support :)
Is Slack still planning to support bolt app, or should developers consider not using this? I mean this seems to me such a fundamental entry point (listening to message), and it has been 2 years since this is opened and no appropriate response yet.
I used this hacky typeguard to get around this quickly:
const isUserBased = <T>(arg: object): arg is T & { user: string } =>
(arg as { user?: string }).user !== undefined;
...
if (isUserBased<KnownEventFromType<'message'>>(context.message)) {
// do stuff with context.message.user
}
I decided to use @slack/bolt
lib with typescript for the 1st time (never really used TS before but i'm proficient in JS).
What a mess to use a correct MessageEvent types, TS always complains about something like Type instantiation is excessively deep and possibly infinite.
.
I'm senior c++ developer so I know what a strong typed language is but really I don't see how to compile my simple code without hacking the TS type system.
import { App } from '@slack/bolt';
// Types definitions for @slack/bolt
import type { SlackEventMiddlewareArgs } from '@slack/bolt';
import type { MeMessageEvent } from '@slack/bolt/dist/types/events/message-events.d.ts';
//import type { MessageEvent } from '@slack/bolt/dist/types/events/base-events.d.ts';
// Custom types definition
type MessageEventArgs = SlackEventMiddlewareArgs<'message'> & { message: GenericMessageEvent };
class ChannelHandler {
private app: App;
private channelMessageHandlers: Map<string, (args: MessageEventArgs) => void>;
constructor(app: App) {
this.app = app;
this.channelMessageHandlers = new Map();
this.setupGlobalMessageListener();
}
private setupGlobalMessageListener(): void {
this.app.message(async (args) => {
const { message } = args;
const handler = this.channelMessageHandlers.get(message.channel);
if (handler) { handler(args); }
});
}
async createChannel(channelName: string, messageHandler: (args: MessageEventArgs) => void): Promise<void> {
try {
const result = await this.app.client.conversations.create({
token: process.env.SLACK_BOT_TOKEN,
name: channelName,
is_private: true
});
const channelId = result.channel?.id;
if (!channelId) { throw new Error(`Channel ID is undefined`); }
console.log(`Channel created: ${channelId}`);
// Register the message handler for this channel
this.channelMessageHandlers.set(channelId, messageHandler);
} catch (error) {
console.error(`Error creating channel: ${error}`);
}
}
}
export type { MessageEventArgs };
export default ChannelHandler;
The type definition for message
is
/**
*
* @param listeners Middlewares that process and react to a message event
*/
message<MiddlewareCustomContext extends StringIndexed = StringIndexed>(...listeners: MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>[]): void;
so args
is MessageEventMiddleware<AppCustomContext & MiddlewareCustomContext>[]
type
and message
is message: EventType extends 'message' ? this['payload'] : never
from
/**
* Arguments which listeners and middleware receive to process an event from Slack's Events API.
*/
export interface SlackEventMiddlewareArgs<EventType extends string = string> {
payload: EventFromType<EventType>;
event: this['payload'];
message: EventType extends 'message' ? this['payload'] : never;
body: EnvelopedEvent<this['payload']>;
say: WhenEventHasChannelContext<this['payload'], SayFn>;
ack: undefined;
}
TL;DR;
I'm lost here, what is the type to define in case of recieving a channel message from a user (user writting into a slack channel) ?
I'm lost here, what is the type to define in case of recieving a channel message from a user (user writting into a slack channel) ?
as @seratch posted in the initial message:
A workaround is to cast the message value by (message as GenericMessageEvent).user but needless to say, this is not great.
Yes I ended up casting the message type as follow, I found it a bit hacky and came here to see if a proper solution existed but apparentlt not.
import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
import type { GenericMessageEvent } from "@slack/bolt/dist/types/events/message-events.d.ts";
type MessageEventArgs = AllMiddlewareArgs & SlackEventMiddlewareArgs<"message">;
const botMessageCallback = async (args: MessageEventArgs) => {
const { message, client, body, say } = args;
try {
const genericMessage = message as GenericMessageEvent;
//...
// Happily using genericMessage.text and genericMessage.channel now, all that type import / cast for that ...
}
}
The developer experience today is pretty hilariously incomplete. It's pretty much unusable if I'm being honest, you couldn't use the SDK today to make even a simple bot that listens to messages from users. I don't know how you'd make something even mildly sophisticated with this tooling.
The suggested hacks above to cast message
to GenericMessageEvent
seem to work well but it isn't clear why that isn't just the type of message
.
Seems the type of message
is being inferred from this Extract<>
type which is essentially looking at SlackEvent
and finding a type where type: 'message'
But the only type in that union with type: 'message'
is ReactionMessageItem
.
Seems like you could fix this by simply adding a "message" type to this union, or even adding GenericMessageEvent
to the union.
Basic message type has no text
property. That is a lack in the SDK.
I successfuly integrated AI services like openai to my Slack App in multiple Workspaces and Channels but the routing was hard to design
We hear that this could be confusing and frustrating. The message event payload data type is a combination of multiple subtypes. Therefore, when it remains of union type, only the most common properties are displayed in your coding editor.
A currently available solution to access text etc. is to check subtype to narrow down possible payload types as demonstrated below:
app.message('hello', async ({ message, say }) => {
// Filter out message events with subtypes (see https://api.slack.com/events/message)
if (message.subtype === undefined || message.subtype === 'bot_message') {
// Inside the if clause, you can access text etc. without type casting
}
});
The full example can be found at: https://github.com/slackapi/bolt-js/blob/%40slack/bolt%403.17.1/examples/getting-started-typescript/src/app.ts#L18-L19
We acknowledge that this isn't a fundamental solution, so we are considering making breaking changes to improve this in future manjor versions: https://github.com/slackapi/bolt-js/pull/1801. Since this is an impactful change for existing apps, we are releasing it with extreme care. The timeline for its release remains undecided. However, once it's launched, handling message events in bolt-js for TypeScript users will be much simplified compared to now.
Ahhh, I haven't seen that before. That makes much more sense.
@seratch Ok, thats prettier than casting but the problem I see is that you have to re-write the if condition inside the callback function that treats message if you use a middleware to filter message like in the following I wrote for my app.
import logger from "../../logger/winston.ts";
import prisma from "../../prisma/client.ts";
import { assistantMessageCallback } from "./assistant-message.ts";
import type { AllMiddlewareArgs, SlackEventMiddlewareArgs, Context, App } from "@slack/bolt";
import type { GenericMessageEvent } from "@slack/bolt/dist/types/events/message-events.d.ts";
import type { MessageElement } from "@slack/web-api/dist/types/response/ConversationsHistoryResponse.js";
type MessageEventArgs = AllMiddlewareArgs & SlackEventMiddlewareArgs<"message">;
export function isBotMessage(message: GenericMessageEvent | MessageElement): boolean {
return message.subtype === "bot_message" || !!message.bot_id;
}
export function isSytemMessage(message: GenericMessageEvent | MessageElement): boolean {
return !!message.subtype && message.subtype !== "bot_message";
}
async function isMessageFromAssistantChannel(message: GenericMessageEvent, context: Context): Promise<boolean> {
const channelId = message.channel;
const assistant = await prisma.assistant.findFirst({
where: {
OR: [{ slackChannelId: channelId }, { privateChannelsId: { has: channelId } }]
}
});
if (assistant) context.assistant = assistant;
return !!assistant;
}
export async function filterAssistantMessages({ message, context, next }: MessageEventArgs) {
const genericMessage = message as GenericMessageEvent;
// Ignore messages without text
if (genericMessage.text === undefined || genericMessage.text.length === 0) return;
// Ignore messages from the bot
if (isBotMessage(genericMessage)) {
logger.debug("Ignoring message from bot");
return;
}
// Ignore system messages
if (isSytemMessage(genericMessage)) {
logger.debug("Ignoring system message");
return;
}
// Accept messages from the assistant channel and store the retrieved assistant in the context
if (await isMessageFromAssistantChannel(genericMessage, context)) await next();
}
const register = (app: App) => {
app.message(filterAssistantMessages, assistantMessageCallback);
};
export default { register };
I don't know if this is the correct way to use the SDK logic but in assistantMessageCallback
I must type cast the message even if it is filtered by the filterAssistantMessages
middleware.
I don't know this could be helpful for your use case, but this repo had a little bit hackey example in the past. The function (msg: MessageEvent): msg is GenericMessageEvent => ...
returns boolean and it helps your code determine type from a union one just by having if/else statement. After this line, the code can access only generic message event data structure without type casting. It seems that your code accepts GenericMessageEvent | MessageElement
type argment, thus this approach may not work smoothly, though.
msg is GenericMessageEvent
is cool and neat, I asked myself if I could filtered in user message
instead of filtered out system or bot message by their subtype. Is it sure that if a GenericMessageEvent
has a defined subtype
, it is not a user message
?
I ran into this today. The DX around this is pretty bad as others have pointed out there. It looks like this will get improved in the 4.0 release. Is there any sense for then that release might come about?
@Scalahansolo we are working first on updating the underlying node SDKs that power bolt-js: https://github.com/slackapi/node-slack-sdk and its various sub-packages. Over the past 6 months or so we have released major new versions for many of these but a few still remain to do (rtm-api, socket-mode and, crucially for this issue, the types sub-package).
I am slowly working my through all the sub-packages; admittedly, it is slow going, and I apologize for that. Our team responsible for the node, java and python SDKs (both lower-level ones as well as the bolt framework) is only a few people and our priorities are currently focussed on other parts of the Slack platform. I am doing my best but releasing major new versions of several packages over the months is challenging and sensitive; we have several tens of thousands of active Slack applications using these modules that we want to take care in supporting through major new version releases. This means doing the utmost to ensure backwards compatibility and providing migration guidance where that is not possible.
I know this is a frustrating experience for TypeScript users leveraging bolt-js. Once the underlying node.js Slack sub-packages are updated to new major versions, we will turn our attention to what should be encompassed in a new major version of bolt-js, which this issue is at the top of the list for.
That all makes total sense and I think provides a good bit of color to this thread / issue / conversation. It was just feeling like at face value that this wasn't going to get addressed given the age of the issue here. Really appreciate all the context and will keep an eye out for updates.
Thanks for the update @filmaj ! Much appreciated :)
My pleasure! FWIW, progress here:
- I have a release candidate for the next major version of
socket-mode
(2.0.0) up on npm and will be stress-testing it this week. -
rtm-api
new major was released a few weeks back β - I will be turning my sights next to the
types
package, which is especially crucial for this issue. I'll be reviewing what types we have in there, which ones are missing, which types from that package are consumed by bolt and what will be needed in that package to improve the experience in bolt-js.
How the community can help: if there are specific issues you have with TypeScript support generally in bolt that is not captured in this issue or any other issues labeled with "TypeScript specific", feel free to at-mention me in any TypeScript-related issue in this repo, or file a new one.
Still a ways away but inching closer!