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

app.message payload arg compatibility in TypeScript #904

Open
seratch opened this issue May 1, 2021 · 33 comments
Open

app.message payload arg compatibility in TypeScript #904

seratch opened this issue May 1, 2021 · 33 comments
Labels
bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented docs M-T: Documentation work only semver:major TypeScript-specific
Milestone

Comments

@seratch
Copy link
Member

seratch commented May 1, 2021

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 #826 (comment)

@gitwave gitwave bot added the untriaged label May 1, 2021
@seratch seratch added bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented docs M-T: Documentation work only semver:minor TypeScript-specific and removed untriaged labels May 1, 2021
@seratch seratch added this to the 3.4.0 milestone May 1, 2021
@seratch
Copy link
Member Author

seratch commented May 12, 2021

Update: #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.

@Oliboy50
Copy link

Oliboy50 commented Jan 21, 2022

👋 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?

@verveguy
Copy link

verveguy commented Feb 8, 2022

My advice: don't bother trying to use Bolt with Typescript at this point. Perhaps one day slack will genuinely prioritize Typescript.

@sangwook-kim
Copy link

스크린샷 2022-02-16 오전 10 20 39
update your tsconfig.json to have "esModuleInterop": true.
It helped me.

@BohdanPetryshyn
Copy link

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}>`);
  }
});

@maksimf
Copy link

maksimf commented Apr 12, 2022

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 :-/

@Onxi95
Copy link

Onxi95 commented Jul 31, 2022

 "@slack/bolt": "^3.12.1",
 "typescript": "^4.7.4"

This one works for me 😄

  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 😃

@lukebelbina
Copy link

Any updates on proper support or updating the documentation so it works as described using TS?

@alshubei
Copy link

alshubei commented Nov 8, 2022

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))
});

@eighteyes
Copy link

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.

@andreasvirkus
Copy link

@seratch what's the state-of-things with proper TypeScript support? Seems we got a lot of hopeful promises ~2 years ago (#826), but the issue was closed out quickly and ever since then, other TS-related issues just get hacky workarounds proposed as accepted solutions 😕

@ethansmith91
Copy link

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.

@TaylorBenner
Copy link

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
}

@seratch seratch modified the milestones: 3.x, 4.0.0 May 18, 2023
@mister-good-deal
Copy link

mister-good-deal commented Nov 11, 2023

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) ?

@dan-perron
Copy link

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.

@mister-good-deal
Copy link

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 ...
    }
}

@peabnuts123
Copy link

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.

image

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'

image

But the only type in that union with type: 'message' is ReactionMessageItem.

image

Seems like you could fix this by simply adding a "message" type to this union, or even adding GenericMessageEvent to the union.

@mister-good-deal
Copy link

mister-good-deal commented Mar 7, 2024

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

@seratch
Copy link
Member Author

seratch commented Mar 7, 2024

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: #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.

@peabnuts123
Copy link

Ahhh, I haven't seen that before. That makes much more sense.

@mister-good-deal
Copy link

@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.

@seratch
Copy link
Member Author

seratch commented Mar 7, 2024

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.

@mister-good-deal
Copy link

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?

@Scalahansolo
Copy link

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?

@filmaj
Copy link
Contributor

filmaj commented Apr 1, 2024

@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.

@Scalahansolo
Copy link

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.

@david1542
Copy link

Thanks for the update @filmaj ! Much appreciated :)

@filmaj
Copy link
Contributor

filmaj commented Apr 24, 2024

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!

@filmaj
Copy link
Contributor

filmaj commented Jun 14, 2024

Update:

@Scalahansolo
Copy link

Is there any recently updates here related to improved types with socket mode?

@filmaj
Copy link
Contributor

filmaj commented Aug 19, 2024

@Scalahansolo re: socket-mode types (I assume you mean types for event payloads), the relevant issue would be this one: slackapi/node-slack-sdk#1395. IMO the @slack/types package should contain event payload types, and socket-mode should consume those.

I am in the process of going through the backlog of bolt-js issues and identifying other areas within bolt-js that may be better consolidated into the types package. The next target for progress here is a major new version for the types package (3.0), so I want to make sure to encompass as many outstanding breaking changes necessary for types in one go.

The current state of the types@3.0.0 milestone is probably what's best to keep track of for progress on that front. slackapi/node-slack-sdk#1395 is also a prerequisite for a new bolt major version, but I believe that could technically be released as a types minor 2.x update.

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 docs M-T: Documentation work only semver:major TypeScript-specific
Projects
None yet
Development

No branches or pull requests