diff --git a/src/structures/Channel.js b/src/structures/Channel.js index c862e1a941bb..9d1825048ffe 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -63,6 +63,16 @@ class Channel extends Base { return new Date(this.createdTimestamp); } + /** + * Whether this Channel is a partial + * This is always false outside of DM channels. + * @type {boolean} + * @readonly + */ + get partial() { + return false; + } + /** * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. * @returns {string} diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 6aeb50c0ee37..4d549cddd94e 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -16,8 +16,10 @@ class DMChannel extends Channel { */ constructor(client, data) { super(client, data); + // Override the channel type so partials have a known type this.type = 'DM'; + /** * A manager of the messages belonging to this channel * @type {MessageManager} diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index b640c904ad81..8d10126aaa77 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -65,7 +65,7 @@ class GuildMember extends Base { if ('user' in data) { /** * The user that this guild member instance represents - * @type {User} + * @type {?User} */ this.user = this.client.users._add(data.user, true); } diff --git a/src/structures/Message.js b/src/structures/Message.js index 41fd35208328..b1eb2be3c20f 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -388,7 +388,7 @@ class Message extends Base { /** * The message contents with all mentions replaced by the equivalent text. * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. - * @type {string} + * @type {?string} * @readonly */ get cleanContent() { diff --git a/typings/index.d.ts b/typings/index.d.ts index ac9bb429f300..eb470b63a64b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -269,6 +269,7 @@ export class Channel extends Base { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; + public readonly partial: false; public type: keyof typeof ChannelTypes; public delete(): Promise; public fetch(force?: boolean): Promise; @@ -456,7 +457,6 @@ export class DMChannel extends TextBasedChannel(Channel, ['bulkDelete']) { public constructor(client: Client, data?: unknown); public messages: MessageManager; public recipient: User; - public readonly partial: false; public type: 'DM'; public fetch(force?: boolean): Promise; } @@ -1187,11 +1187,11 @@ export class MessageReaction { private _emoji: GuildEmoji | ReactionEmoji; public readonly client: Client; - public count: number | null; + public count: number; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message | PartialMessage; - public readonly partial: boolean; + public readonly partial: false; public users: ReactionUserManager; public remove(): Promise; public fetch(): Promise; @@ -2493,6 +2493,8 @@ export interface AddGuildMemberOptions { export type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'jpeg' | 'gif'; +export type AllowedPartial = User | Channel | GuildMember | Message | MessageReaction; + export type AllowedThreadTypeForNewsChannel = 'GUILD_NEWS_THREAD' | 10; export type AllowedThreadTypeForTextChannel = 'GUILD_PUBLIC_THREAD' | 'GUILD_PRIVATE_THREAD' | 11 | 12; @@ -2809,10 +2811,10 @@ export interface ClientEvents { messageCreate: [message: Message]; messageDelete: [message: Message | PartialMessage]; messageReactionRemoveAll: [message: Message | PartialMessage]; - messageReactionRemoveEmoji: [reaction: MessageReaction]; + messageReactionRemoveEmoji: [reaction: MessageReaction | PartialMessageReaction]; messageDeleteBulk: [messages: Collection]; - messageReactionAdd: [message: MessageReaction, user: User | PartialUser]; - messageReactionRemove: [reaction: MessageReaction, user: User | PartialUser]; + messageReactionAdd: [message: MessageReaction | PartialMessageReaction, user: User | PartialUser]; + messageReactionRemove: [reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser]; messageUpdate: [oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage]; presenceUpdate: [oldPresence: Presence | null, newPresence: Presence]; rateLimit: [rateLimitData: RateLimitData]; @@ -3790,35 +3792,6 @@ export interface PresenceData { export type PresenceResolvable = Presence | UserResolvable | Snowflake; -export type Partialize = { - readonly client: Client; - readonly createdAt: Date; - readonly createdTimestamp: number; - deleted: boolean; - id: Snowflake; - partial: true; - fetch(): Promise; -} & { - [K in keyof Omit< - T, - 'client' | 'createdAt' | 'createdTimestamp' | 'id' | 'partial' | 'fetch' | 'deleted' | O - >]: T[K] extends (...args: any[]) => void ? T[K] : T[K] | null; -}; - -export interface PartialDMChannel - extends Partialize< - DMChannel, - 'lastMessage' | 'lastMessageId' | 'messages' | 'recipient' | 'type' | 'typing' | 'typingCount' - > { - lastMessage: null; - lastMessageId: undefined; - messages: MessageManager; - recipient: User | PartialUser; - type: 'DM'; - readonly typing: boolean; - readonly typingCount: number; -} - export interface PartialChannelData { id?: Snowflake | number; name: string; @@ -3828,62 +3801,29 @@ export interface PartialChannelData { permissionOverwrites?: PartialOverwriteData[]; } -export interface PartialGuildMember - extends Partialize< - GuildMember, - | 'bannable' - | 'displayColor' - | 'displayHexColor' - | 'displayName' - | 'guild' - | 'kickable' - | 'permissions' - | 'roles' - | 'manageable' - | 'presence' - | 'voice' - > { - readonly bannable: boolean; - readonly displayColor: number; - readonly displayHexColor: HexColorString; - readonly displayName: string; - guild: Guild; - readonly manageable: boolean; - joinedAt: null; - joinedTimestamp: null; - readonly kickable: boolean; - readonly permissions: GuildMember['permissions']; - readonly presence: GuildMember['presence']; - readonly roles: GuildMember['roles']; - readonly voice: GuildMember['voice']; +export type Partialize< + T extends AllowedPartial, + N extends keyof T | null = null, + M extends keyof T | null = null, + E extends keyof T | '' = '', +> = { + readonly client: Client; + id: Snowflake; + partial: true; +} & { + [K in keyof Omit]: K extends N ? null : K extends M ? T[K] | null : T[K]; +}; + +export interface PartialDMChannel extends Partialize { + lastMessageId: undefined; } +export interface PartialGuildMember extends Partialize {} + export interface PartialMessage - extends Partialize< - Message, - | 'attachments' - | 'channel' - | 'deletable' - | 'crosspostable' - | 'editable' - | 'mentions' - | 'pinnable' - | 'url' - | 'flags' - | 'embeds' - > { - attachments: Message['attachments']; - channel: Message['channel']; - readonly deletable: boolean; - readonly crosspostable: boolean; - readonly editable: boolean; - embeds: Message['embeds']; - flags: Message['flags']; - mentions: Message['mentions']; - readonly pinnable: boolean; - reactions: Message['reactions']; - readonly url: string; -} + extends Partialize {} + +export interface PartialMessageReaction extends Partialize {} export interface PartialOverwriteData { id: Snowflake | number; @@ -3898,14 +3838,7 @@ export interface PartialRoleData extends RoleData { export type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' | 'MESSAGE' | 'REACTION'; -export interface PartialUser - extends Omit, 'deleted'> { - bot: null; - flags: User['flags']; - system: null; - readonly tag: null; - username: null; -} +export interface PartialUser extends Partialize {} export type PresenceStatusData = ClientPresenceStatus | 'invisible'; diff --git a/typings/index.ts b/typings/index.ts index 32abbf3f133d..7d4650ae59af 100644 --- a/typings/index.ts +++ b/typings/index.ts @@ -581,3 +581,25 @@ declare const guildEmojiManager: GuildEmojiManager; assertType>>(guildEmojiManager.fetch()); assertType>>(guildEmojiManager.fetch(undefined, {})); assertType>(guildEmojiManager.fetch('0')); + +// Test partials structures +client.on('typingStart', (channel, user) => { + if (channel.partial) assertType(channel.lastMessageId); + if (user.partial) return assertType(user.username); + assertType(user.username); +}); + +client.on('guildMemberRemove', member => { + if (member.partial) return assertType(member.joinedAt); + assertType(member.joinedAt); +}); + +client.on('messageReactionAdd', async reaction => { + if (reaction.partial) { + assertType(reaction.count); + reaction = await reaction.fetch(); + } + assertType(reaction.count); + if (reaction.message.partial) return assertType(reaction.message.content); + assertType(reaction.message.content); +});