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