Skip to content

Commit

Permalink
refactor(Structure): use unknown to store in kData
Browse files Browse the repository at this point in the history
  • Loading branch information
ckohen committed Dec 6, 2023
1 parent 98b83f1 commit fd45e9c
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 27 deletions.
17 changes: 10 additions & 7 deletions packages/structures/src/Structure.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { data as kData } from './utils/symbols.js';
import type { ReplaceOmittedWithUnknown } from './utils/types.js';

/**
* Explanation of the type complexity surround Structure:
*
* There are two layers of Omitted generics, one here, which allows omitting things at the library level so we do not accidentally
* access them. This generic should only be used within this library, as passing it higher will make it impossible to safely access kData
* access them, in addition to whatever the user does at the layer above.
*
* The second layer, in the exported structure is effectively a type cast that allows the getters types to match whatever data template
* is used. In order for this to function properly, we need to cast the return values of the getters,
* utilizing this level of Omit to guarantee the original type or never.
* The second layer, in the exported structure is effectively a type cast that allows the getters types to match whatever data template is ued
*
* In order to safely set and access this data, the constructor and patch take data as "partial" and forcibly assigns it to kData. To acommodate this,
* kData stores properties as `unknown` when it is omitted, which allows accessing the property in getters even when it may not actually be present.
* This is the most technically correct way of represnting the value, especially since there is no way to guarantee runtime matches the "type cast."
*
* @internal
*/

export abstract class Structure<DataType, Omitted extends keyof DataType | '' = ''> {
protected [kData]: Readonly<Omit<DataType, Omitted>>;
protected [kData]: Readonly<ReplaceOmittedWithUnknown<Omitted, DataType>>;

protected constructor(data: Readonly<Omit<DataType, Omitted>>, { template }: { template?: {} } = {}) {
protected constructor(data: Readonly<Partial<DataType>>, { template }: { template?: {} } = {}) {
this[kData] = Object.assign(template ? Object.create(template) : {}, data);
}

protected patch(data: Readonly<Partial<Omit<DataType, Omitted>>>, { template }: { template?: {} } = {}): this {
protected patch(data: Readonly<Partial<DataType>>, { template }: { template?: {} } = {}): this {
this[kData] = Object.assign(template ? Object.create(template) : {}, this[kData], data);
return this;
}
Expand Down
40 changes: 20 additions & 20 deletions packages/structures/src/users/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ export function getUserDataTemplate(): Readonly<Partial<APIUser>> {
/**
* Represents any user on Discord.
*/
export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<APIUser> {
export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<APIUser, Omitted> {
public constructor(
/**
* The raw data received from the API for the user
*/
data: Omit<APIUser, Omitted>,
) {
// Cast here so the getters can access the properties, and provide typesafety by explicitly assigning return values
super(data as APIUser, { template: UserDataTemplate });
super(data, { template: UserDataTemplate });
}

public override patch(data: Partial<APIUser>) {
Expand All @@ -45,134 +45,134 @@ export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<API
* The user's id
*/
public get id() {
return this[kData].id as 'id' extends Omitted ? never : APIUser['id'];
return this[kData].id;
}

/**
* The username of the user
*/
public get username() {
return this[kData].username as 'username' extends Omitted ? never : APIUser['username'];
return this[kData].username;
}

/**
* The user's 4 digit tag, if a bot or not migrated to unique usernames
*/
public get discriminator() {
return this[kData].discriminator as 'discriminator' extends Omitted ? never : APIUser['discriminator'];
return this[kData].discriminator;
}

/**
* The user's display name, the application name for bots
*/
public get globalName() {
return this[kData].global_name as 'global_name' extends Omitted ? never : APIUser['global_name'];
return this[kData].global_name;
}

/**
* The user avatar's hash
*/
public get avatar() {
return this[kData].avatar as 'avatar' extends Omitted ? never : APIUser['avatar'];
return this[kData].avatar;
}

/**
* Whether the user is a bot
*/
public get bot() {
return (this[kData].bot ?? false) as 'bot' extends Omitted ? never : APIUser['bot'];
return this[kData].bot ?? false;
}

/**
* Whether the user is an Official Discord System user
*/
public get system() {
return (this[kData].system ?? false) as 'system' extends Omitted ? never : APIUser['system'];
return this[kData].system ?? false;
}

/**
* Whether the user has mfa enabled
* <info>This property is only set when the user was fetched with an OAuth2 token and the `identify` scope</info>
*/
public get mfaEnabled() {
return this[kData].mfa_enabled as 'mfa_enabled' extends Omitted ? never : APIUser['mfa_enabled'];
return this[kData].mfa_enabled;
}

/**
* The user's banner hash
* <info>This property is only set when the user was manually fetched</info>
*/
public get banner() {
return this[kData].banner as 'banner' extends Omitted ? never : APIUser['banner'];
return this[kData].banner;
}

/**
* The base 10 accent color of the user's banner
* <info>This property is only set when the user was manually fetched</info>
*/
public get accentColor() {
return this[kData].accent_color as 'accent_color' extends Omitted ? never : APIUser['accent_color'];
return this[kData].accent_color;
}

/**
* The user's primary discord language
* <info>This property is only set when the user was fetched with an Oauth2 token and the `identify` scope</info>
*/
public get locale() {
return this[kData].locale as 'locale' extends Omitted ? never : APIUser['locale'];
return this[kData].locale;
}

/**
* Whether the email on the user's account has been verified
* <info>This property is only set when the user was fetched with an OAuth2 token and the `email` scope</info>
*/
public get verified() {
return this[kData].verified as 'verified' extends Omitted ? never : APIUser['verified'];
return this[kData].verified;
}

/**
* The user's email
* <info>This property is only set when the user was fetched with an OAuth2 token and the `email` scope</info>
*/
public get email() {
return this[kData].email as 'email' extends Omitted ? never : APIUser['email'];
return this[kData].email;
}

/**
* The flags on the user's account
* <info> This property is only set when the user was fetched with an OAuth2 token and the `identity` scope</info>
*/
public get flags() {
return this[kData].flags as 'flags' extends Omitted ? never : APIUser['flags'];
return this[kData].flags;
}

/**
* The type of nitro subscription on the user's account
* <info>This property is only set when the user was fetched with an OAuth2 token and the `identify` scope</info>
*/
public get premiumType() {
return this[kData].premium_type as 'premium_type' extends Omitted ? never : APIUser['premium_type'];
return this[kData].premium_type;
}

/**
* The public flags for the user
*/
public get publicFlags() {
return this[kData].public_flags as 'public_flags' extends Omitted ? never : APIUser['public_flags'];
return this[kData].public_flags;
}

/**
* The user's avatar decoration hash
*/
public get avatarDecoration() {
return this[kData].avatar_decoration as 'avatar_decoration' extends Omitted ? never : APIUser['avatar_decoration'];
return this[kData].avatar_decoration;
}

/**
* The timestamp the user was created at
*/
public get createdTimestamp() {
return this.id ? DiscordSnowflake.timestampFrom(this.id) : null;
return typeof this.id === 'string' ? DiscordSnowflake.timestampFrom(this.id) : null;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/structures/src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ReplaceOmittedWithUnknown<Omitted extends keyof Data | '', Data> = {
[Key in keyof Data]: Key extends Omitted ? unknown : Data[Key];
};

0 comments on commit fd45e9c

Please sign in to comment.