slackapi / bolt-js

A framework to build Slack apps using JavaScript
https://tools.slack.dev/bolt-js/
MIT License
2.74k stars 393 forks source link

app.message payload arg compatibility in TypeScript #904

Open seratch opened 3 years ago

seratch commented 3 years ago

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

seratch commented 3 years ago

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.

Oliboy50 commented 2 years ago

πŸ‘‹ 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 commented 2 years ago

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

sangwook-kim commented 2 years ago

스크란샷 2022-02-16 α„‹α…©α„Œα…₯ᆫ 10 20 39 update your tsconfig.json to have "esModuleInterop": true. It helped me.

BohdanPetryshyn commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago
 "@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:

lukebelbina commented 2 years ago

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

alshubei commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 πŸ˜•

davidalpert commented 1 year ago

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?

dallonasnes commented 1 year ago

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

ethansmith91 commented 1 year ago

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 commented 1 year ago

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
}
mister-good-deal commented 11 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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.

peabnuts123 commented 7 months ago

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

mister-good-deal commented 7 months ago

@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 commented 7 months ago

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 commented 7 months ago

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 commented 6 months ago

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 commented 6 months ago

@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 commented 6 months ago

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 commented 5 months ago

Thanks for the update @filmaj ! Much appreciated :)

filmaj commented 5 months ago

My pleasure! FWIW, progress here:

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 commented 4 months ago

Update:

Scalahansolo commented 2 months ago

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

filmaj commented 2 months ago

@Scalahansolo re: socket-mode types (I assume you mean types for event payloads), the relevant issue would be this one: https://github.com/slackapi/node-slack-sdk/issues/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. https://github.com/slackapi/node-slack-sdk/issues/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.