Skip to content

Commit

Permalink
types: Inference of guild in MessageManager (#8538)
Browse files Browse the repository at this point in the history
* types: better inference of message manager

* types: alter helper methods

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
Jiralite and kodiakhq[bot] committed Aug 22, 2022
1 parent 27bbc8f commit 6bb1474
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 43 deletions.
100 changes: 62 additions & 38 deletions packages/discord.js/typings/index.d.ts
Expand Up @@ -123,6 +123,7 @@ import {
FormattingPatterns,
APIEmbedProvider,
AuditLogOptionsType,
TextChannelType,
} from 'discord-api-types/v10';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
Expand Down Expand Up @@ -521,7 +522,7 @@ export class BaseGuildEmoji extends Emoji {
public requiresColons: boolean | null;
}

export class BaseGuildTextChannel extends TextBasedChannelMixin(GuildChannel) {
export class BaseGuildTextChannel extends TextBasedChannelMixin(GuildChannel, true) {
protected constructor(guild: Guild, data?: RawGuildChannelData, client?: Client, immediatePatch?: boolean);
public defaultAutoArchiveDuration?: ThreadAutoArchiveDuration;
public rateLimitPerUser: number | null;
Expand Down Expand Up @@ -1049,7 +1050,7 @@ export class DataResolver extends null {
public static resolveGuildTemplateCode(data: GuildTemplateResolvable): string;
}

export class DMChannel extends TextBasedChannelMixin(BaseChannel, [
export class DMChannel extends TextBasedChannelMixin(BaseChannel, false, [
'bulkDelete',
'fetchWebhooks',
'createWebhook',
Expand Down Expand Up @@ -1675,16 +1676,16 @@ export interface MappedInteractionTypes<Cached extends boolean = boolean> {
[ComponentType.SelectMenu]: SelectMenuInteraction<WrapBooleanCache<Cached>>;
}

export class Message<Cached extends boolean = boolean> extends Base {
private readonly _cacheType: Cached;
export class Message<InGuild extends boolean = boolean> extends Base {
private readonly _cacheType: InGuild;
private constructor(client: Client, data: RawMessageData);
private _patch(data: RawPartialMessageData | RawMessageData): void;

public activity: MessageActivity | null;
public applicationId: Snowflake | null;
public attachments: Collection<Snowflake, Attachment>;
public author: User;
public get channel(): If<Cached, GuildTextBasedChannel, TextBasedChannel>;
public get channel(): If<InGuild, GuildTextBasedChannel, TextBasedChannel>;
public channelId: Snowflake;
public get cleanContent(): string;
public components: ActionRow<MessageActionRowComponent>[];
Expand All @@ -1698,8 +1699,8 @@ export class Message<Cached extends boolean = boolean> extends Base {
public editedTimestamp: number | null;
public embeds: Embed[];
public groupActivityApplication: ClientApplication | null;
public guildId: If<Cached, Snowflake>;
public get guild(): If<Cached, Guild>;
public guildId: If<InGuild, Snowflake>;
public get guild(): If<InGuild, Guild>;
public get hasThread(): boolean;
public id: Snowflake;
public interaction: MessageInteraction | null;
Expand All @@ -1720,30 +1721,30 @@ export class Message<Cached extends boolean = boolean> extends Base {
public flags: Readonly<MessageFlagsBitField>;
public reference: MessageReference | null;
public awaitMessageComponent<T extends MessageComponentType>(
options?: AwaitMessageCollectorOptionsParams<T, Cached>,
): Promise<MappedInteractionTypes<Cached>[T]>;
options?: AwaitMessageCollectorOptionsParams<T, InGuild>,
): Promise<MappedInteractionTypes<InGuild>[T]>;
public awaitReactions(options?: AwaitReactionsOptions): Promise<Collection<Snowflake | string, MessageReaction>>;
public createReactionCollector(options?: ReactionCollectorOptions): ReactionCollector;
public createMessageComponentCollector<T extends MessageComponentType>(
options?: MessageCollectorOptionsParams<T, Cached>,
): InteractionCollector<MappedInteractionTypes<Cached>[T]>;
public delete(): Promise<Message>;
public edit(content: string | MessageEditOptions | MessagePayload): Promise<Message>;
options?: MessageCollectorOptionsParams<T, InGuild>,
): InteractionCollector<MappedInteractionTypes<InGuild>[T]>;
public delete(): Promise<Message<InGuild>>;
public edit(content: string | MessageEditOptions | MessagePayload): Promise<Message<InGuild>>;
public equals(message: Message, rawData: unknown): boolean;
public fetchReference(): Promise<Message>;
public fetchReference(): Promise<Message<InGuild>>;
public fetchWebhook(): Promise<Webhook>;
public crosspost(): Promise<Message>;
public fetch(force?: boolean): Promise<Message>;
public pin(reason?: string): Promise<Message>;
public crosspost(): Promise<Message<InGuild>>;
public fetch(force?: boolean): Promise<Message<InGuild>>;
public pin(reason?: string): Promise<Message<InGuild>>;
public react(emoji: EmojiIdentifierResolvable): Promise<MessageReaction>;
public removeAttachments(): Promise<Message>;
public reply(options: string | MessagePayload | ReplyMessageOptions): Promise<Message>;
public removeAttachments(): Promise<Message<InGuild>>;
public reply(options: string | MessagePayload | ReplyMessageOptions): Promise<Message<InGuild>>;
public resolveComponent(customId: string): MessageActionRowComponent | null;
public startThread(options: StartThreadOptions): Promise<AnyThreadChannel>;
public suppressEmbeds(suppress?: boolean): Promise<Message>;
public suppressEmbeds(suppress?: boolean): Promise<Message<InGuild>>;
public toJSON(): unknown;
public toString(): string;
public unpin(reason?: string): Promise<Message>;
public unpin(reason?: string): Promise<Message<InGuild>>;
public inGuild(): this is Message<true> & this;
}

Expand Down Expand Up @@ -2506,7 +2507,11 @@ export interface PrivateThreadChannel extends ThreadChannel {
type: ChannelType.GuildPrivateThread;
}

export class ThreadChannel extends TextBasedChannelMixin(BaseChannel, ['fetchWebhooks', 'createWebhook', 'setNSFW']) {
export class ThreadChannel extends TextBasedChannelMixin(BaseChannel, true, [
'fetchWebhooks',
'createWebhook',
'setNSFW',
]) {
private constructor(guild: Guild, data?: RawThreadChannelData, client?: Client, fromInteraction?: boolean);
public archived: boolean | null;
public get archivedAt(): Date | null;
Expand Down Expand Up @@ -2752,7 +2757,10 @@ export type ComponentData =
| ModalActionRowComponentData
| ActionRowData<MessageActionRowComponentData | ModalActionRowComponentData>;

export class VoiceChannel extends TextBasedChannelMixin(BaseGuildVoiceChannel, ['lastPinTimestamp', 'lastPinAt']) {
export class VoiceChannel extends TextBasedChannelMixin(BaseGuildVoiceChannel, true, [
'lastPinTimestamp',
'lastPinAt',
]) {
public videoQualityMode: VideoQualityMode | null;
public get speakable(): boolean;
public type: ChannelType.GuildVoice;
Expand Down Expand Up @@ -3518,15 +3526,22 @@ export class GuildMemberRoleManager extends DataManager<Snowflake, Role, RoleRes
): Promise<GuildMember>;
}

export class MessageManager extends CachedManager<Snowflake, Message, MessageResolvable> {
export class MessageManager<InGuild extends boolean = boolean> extends CachedManager<
Snowflake,
Message<InGuild>,
MessageResolvable
> {
private constructor(channel: TextBasedChannel, iterable?: Iterable<RawMessageData>);
public channel: TextBasedChannel;
public crosspost(message: MessageResolvable): Promise<Message>;
public channel: If<InGuild, GuildTextBasedChannel, TextBasedChannel>;
public crosspost(message: MessageResolvable): Promise<Message<InGuild>>;
public delete(message: MessageResolvable): Promise<void>;
public edit(message: MessageResolvable, options: string | MessagePayload | MessageEditOptions): Promise<Message>;
public fetch(options: MessageResolvable | FetchMessageOptions): Promise<Message>;
public fetch(options?: FetchMessagesOptions): Promise<Collection<Snowflake, Message>>;
public fetchPinned(cache?: boolean): Promise<Collection<Snowflake, Message>>;
public edit(
message: MessageResolvable,
options: string | MessagePayload | MessageEditOptions,
): Promise<Message<InGuild>>;
public fetch(options: MessageResolvable | FetchMessageOptions): Promise<Message<InGuild>>;
public fetch(options?: FetchMessagesOptions): Promise<Collection<Snowflake, Message<InGuild>>>;
public fetchPinned(cache?: boolean): Promise<Collection<Snowflake, Message<InGuild>>>;
public react(message: MessageResolvable, emoji: EmojiIdentifierResolvable): Promise<void>;
public pin(message: MessageResolvable, reason?: string): Promise<void>;
public unpin(message: MessageResolvable, reason?: string): Promise<void>;
Expand Down Expand Up @@ -3649,22 +3664,31 @@ export class VoiceStateManager extends CachedManager<Snowflake, VoiceState, type
// to each of those classes

export type Constructable<T> = abstract new (...args: any[]) => T;
export function PartialTextBasedChannel<T>(Base?: Constructable<T>): Constructable<T & PartialTextBasedChannelFields>;
export function TextBasedChannelMixin<T, I extends keyof TextBasedChannelFields = never>(
export function PartialTextBasedChannel<T>(
Base?: Constructable<T>,
): Constructable<T & PartialTextBasedChannelFields<false>>;

export function TextBasedChannelMixin<
T,
InGuild extends boolean = boolean,
I extends keyof TextBasedChannelFields<InGuild> = never,
>(
Base?: Constructable<T>,
inGuild?: InGuild,
ignore?: I[],
): Constructable<T & Omit<TextBasedChannelFields, I>>;
): Constructable<T & Omit<TextBasedChannelFields<InGuild>, I>>;

export interface PartialTextBasedChannelFields {
send(options: string | MessagePayload | MessageOptions): Promise<Message>;
export interface PartialTextBasedChannelFields<InGuild extends boolean = boolean> {
send(options: string | MessagePayload | MessageOptions): Promise<Message<InGuild>>;
}

export interface TextBasedChannelFields extends PartialTextBasedChannelFields {
export interface TextBasedChannelFields<InGuild extends boolean = boolean>
extends PartialTextBasedChannelFields<InGuild> {
lastMessageId: Snowflake | null;
get lastMessage(): Message | null;
lastPinTimestamp: number | null;
get lastPinAt(): Date | null;
messages: MessageManager;
messages: MessageManager<InGuild>;
awaitMessageComponent<T extends MessageComponentType>(
options?: AwaitMessageCollectorOptionsParams<T, true>,
): Promise<MappedInteractionTypes[T]>;
Expand Down Expand Up @@ -5521,7 +5545,7 @@ export type Channel =
| AnyThreadChannel
| VoiceChannel;

export type TextBasedChannel = Extract<Channel, { messages: MessageManager }>;
export type TextBasedChannel = Exclude<Extract<Channel, { type: TextChannelType }>, PartialGroupDMChannel>;

export type TextBasedChannelTypes = TextBasedChannel['type'];

Expand Down
38 changes: 33 additions & 5 deletions packages/discord.js/typings/index.test-d.ts
Expand Up @@ -1049,11 +1049,11 @@ declare const user: User;
declare const guildMember: GuildMember;

// Test whether the structures implement send
expectType<TextBasedChannelFields['send']>(dmChannel.send);
expectType<TextBasedChannelFields['send']>(threadChannel.send);
expectType<TextBasedChannelFields['send']>(newsChannel.send);
expectType<TextBasedChannelFields['send']>(textChannel.send);
expectType<TextBasedChannelFields['send']>(voiceChannel.send);
expectType<TextBasedChannelFields<false>['send']>(dmChannel.send);
expectType<TextBasedChannelFields<true>['send']>(threadChannel.send);
expectType<TextBasedChannelFields<true>['send']>(newsChannel.send);
expectType<TextBasedChannelFields<true>['send']>(textChannel.send);
expectType<TextBasedChannelFields<true>['send']>(voiceChannel.send);
expectAssignable<PartialTextBasedChannelFields>(user);
expectAssignable<PartialTextBasedChannelFields>(guildMember);

Expand Down Expand Up @@ -1190,6 +1190,34 @@ declare const guildChannelManager: GuildChannelManager;
expectType<Promise<Collection<Snowflake, AnyChannel>>>(guildChannelManager.fetch());
expectType<Promise<Collection<Snowflake, AnyChannel>>>(guildChannelManager.fetch(undefined, {}));
expectType<Promise<AnyChannel | null>>(guildChannelManager.fetch('0'));

const channel = guildChannelManager.cache.first()!;

if (channel.isTextBased()) {
const { messages } = channel;
const message = await messages.fetch('123');
expectType<MessageManager<true>>(messages);
expectType<Promise<Message<true>>>(messages.crosspost('1234567890'));
expectType<Promise<Message<true>>>(messages.edit('1234567890', 'text'));
expectType<Promise<Message<true>>>(messages.fetch('1234567890'));
expectType<Promise<Collection<Snowflake, Message<true>>>>(messages.fetchPinned());
expectType<Guild>(message.guild);
expectType<Snowflake>(message.guildId);
expectType<GuildTextBasedChannel>(message.channel.messages.channel);
}
}

{
const { messages } = dmChannel;
const message = await messages.fetch('123');
expectType<MessageManager<false>>(messages);
expectType<Promise<Message<false>>>(messages.crosspost('1234567890')); // This shouldn't even exist!
expectType<Promise<Message<false>>>(messages.edit('1234567890', 'text'));
expectType<Promise<Message<false>>>(messages.fetch('1234567890'));
expectType<Promise<Collection<Snowflake, Message<false>>>>(messages.fetchPinned());
expectType<null>(message.guild);
expectType<null>(message.guildId);
expectType<TextBasedChannel>(message.channel.messages.channel);
}

declare const messageManager: MessageManager;
Expand Down

0 comments on commit 6bb1474

Please sign in to comment.