From 03d5549461ef29a191f9a32d4a2f45ac3c58f0cd Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sun, 29 Aug 2021 11:25:58 -0400 Subject: [PATCH] typings: Allow message component interaction collectors to infer collected interaction types (#6476) --- typings/index.d.ts | 82 ++++++++++++++++++++++++++++++++++++++-------- typings/tests.ts | 32 +++++++++++++++++- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 480803fa3ce0..39dc7b1c0a47 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1103,6 +1103,46 @@ export class LimitedCollection extends Collection { public static filterByLifetime(options?: LifetimeFilterOptions): SweepFilter; } +// This is a general conditional type utility that allows for specific union members to be extracted given +// a tagged union. +type TaggedUnion = T extends Record + ? T + : T extends Record + ? V extends U + ? T + : never + : never; + +// This creates a map of MessageComponentTypes to their respective `InteractionCollectorOptionsResolvable` variant. +type CollectorOptionsTypeResolver = { + readonly [T in U['componentType']]: TaggedUnion; +}; + +// This basically says "Given a `InteractionCollectorOptionsResolvable` variant", I'll give the corresponding +// `InteractionCollector` variant back. +type ConditionalInteractionCollectorType = + T extends InteractionCollectorOptions + ? InteractionCollector + : InteractionCollector; + +// This maps each componentType key to each variant. +type MappedInteractionCollectorOptions = CollectorOptionsTypeResolver; + +// Converts mapped types to complimentary collector types. +type InteractionCollectorReturnType = T extends + | MessageComponentType + | MessageComponentTypes + ? ConditionalInteractionCollectorType + : InteractionCollector; + +type MessageCollectorOptionsParams = + | ({ componentType?: T } & InteractionCollectorOptionsResolvable) + | InteractionCollectorOptions; + +type AwaitMessageCollectorOptionsParams = + | ({ componentType?: T } & Pick>) + | AwaitMessageComponentOptions; + export class Message extends Base { public constructor(client: Client, data: RawMessageData); private _patch(data: RawPartialMessageData, partial: true): void; @@ -1150,14 +1190,14 @@ export class Message extends Base { public webhookId: Snowflake | null; public flags: Readonly; public reference: MessageReference | null; - public awaitMessageComponent( - options?: AwaitMessageComponentOptions, - ): Promise; + public awaitMessageComponent( + options?: AwaitMessageCollectorOptionsParams, + ): Promise>; public awaitReactions(options?: AwaitReactionsOptions): Promise>; public createReactionCollector(options?: ReactionCollectorOptions): ReactionCollector; - public createMessageComponentCollector( - options?: InteractionCollectorOptions, - ): InteractionCollector; + public createMessageComponentCollector< + T extends MessageComponentType | MessageComponentTypes | undefined = undefined, + >(options?: MessageCollectorOptionsParams): InteractionCollectorReturnType; public delete(): Promise; public edit(content: string | MessageEditOptions | MessagePayload): Promise; public equals(message: Message, rawData: unknown): boolean; @@ -2711,18 +2751,17 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date | null; - awaitMessageComponent( - options?: AwaitMessageComponentOptions, - ): Promise; + awaitMessageComponent( + options?: AwaitMessageCollectorOptionsParams, + ): Promise>; awaitMessages(options?: AwaitMessagesOptions): Promise>; bulkDelete( messages: Collection | readonly MessageResolvable[] | number, filterOld?: boolean, ): Promise>; - createMessageComponentCollector( - options?: InteractionCollectorOptions, - ): InteractionCollector; - createMessageCollector(options?: MessageCollectorOptions): MessageCollector; + createMessageComponentCollector( + options?: MessageCollectorOptionsParams, + ): InteractionCollectorReturnType; sendTyping(): Promise; } @@ -3959,6 +3998,23 @@ export interface InteractionCollectorOptions extends Coll message?: Message | APIMessage; } +export interface ButtonInteractionCollectorOptions extends InteractionCollectorOptions { + componentType: 'BUTTON' | MessageComponentTypes.BUTTON; +} + +export interface SelectMenuInteractionCollectorOptions extends InteractionCollectorOptions { + componentType: 'SELECT_MENU' | MessageComponentTypes.SELECT_MENU; +} + +export interface MessageInteractionCollectorOptions extends InteractionCollectorOptions { + componentType: 'ACTION_ROW' | MessageComponentTypes.ACTION_ROW; +} + +export type InteractionCollectorOptionsResolvable = + | MessageInteractionCollectorOptions + | SelectMenuInteractionCollectorOptions + | ButtonInteractionCollectorOptions; + export interface InteractionDeferReplyOptions { ephemeral?: boolean; fetchReply?: boolean; diff --git a/typings/tests.ts b/typings/tests.ts index d9b67ddd87f3..307fcc31680d 100644 --- a/typings/tests.ts +++ b/typings/tests.ts @@ -8,6 +8,7 @@ import { ApplicationCommandResolvable, ApplicationCommandSubCommandData, ApplicationCommandSubGroupData, + ButtonInteraction, CacheFactory, Caches, CategoryChannel, @@ -31,12 +32,14 @@ import { GuildResolvable, Intents, Interaction, + InteractionCollector, LimitedCollection, Message, MessageActionRow, MessageAttachment, MessageButton, MessageCollector, + MessageComponentInteraction, MessageEmbed, MessageManager, MessageReaction, @@ -49,6 +52,7 @@ import { ReactionCollector, Role, RoleManager, + SelectMenuInteraction, Serialized, ShardClientUtil, ShardingManager, @@ -469,7 +473,8 @@ client.on('messageReactionRemoveAll', async message => { // This is to check that stuff is the right type declare const assertIsMessage: (m: Promise) => void; -client.on('messageCreate', ({ channel }) => { +client.on('messageCreate', message => { + const { channel } = message; assertIsMessage(channel.send('string')); assertIsMessage(channel.send({})); assertIsMessage(channel.send({ embeds: [] })); @@ -484,6 +489,31 @@ client.on('messageCreate', ({ channel }) => { channel.send(); // @ts-expect-error channel.send({ another: 'property' }); + + // Check collector creations. + + // Verify that buttons interactions are inferred. + const buttonCollector = message.createMessageComponentCollector({ componentType: 'BUTTON' }); + assertType>>( + message.awaitMessageComponent({ componentType: 'BUTTON' }), + ); + assertType>(buttonCollector); + + // Verify that select menus interaction are inferred. + const selectMenuCollector = message.createMessageComponentCollector({ componentType: 'SELECT_MENU' }); + assertType>>( + message.awaitMessageComponent({ componentType: 'SELECT_MENU' }), + ); + assertType>(selectMenuCollector); + + // Verify that message component interactions are default collected types. + const defaultCollector = message.createMessageComponentCollector(); + assertType>>(message.awaitMessageComponent()); + assertType>(defaultCollector); + + // Verify that additional options don't affect default collector types. + const semiDefaultCollector = message.createMessageComponentCollector({ interactionType: 'APPLICATION_COMMAND' }); + assertType>(semiDefaultCollector); }); client.on('interaction', async interaction => {