feat(types/util): add case utility types

This commit is contained in:
ayntee
2021-03-14 19:50:22 +04:00
parent 2d0b96c1e2
commit 9ec5e65f38
3 changed files with 135 additions and 184 deletions

View File

@@ -1,166 +0,0 @@
# Discordeno Typings Guidelines / Explanations
Discordeno has a certain standard guidelines for our typings. Please follow this
as best as possible when contributing to the library.
1. Discordeno Types
These types will be specifically used by the end user for functions inside
Discordeno.
Example:
```ts
interface EditMemberOptions {
/** Value to set users nickname to. Requires MANAGE_NICKNAMES permission. */
nick?: string;
/** Array of role ids the member will have after this edit. Useful for adding/removing multiple roles in 1 API call. Requires MANAGE_ROLES permission. */
roles?: string[];
/** Whether the user is muted in voice channels. Requires MUTE_MEMBERS permission. */
mute?: boolean;
/** Whether the user is deafened in voice channels. Requires DEAFEN_MEMBERS permission. */
deaf?: boolean;
/** The id of the channel to move user to if they are connected to voice. To kick the user from their current channel, set to null. Requires MOVE_MEMBERS permission. When moving members to channels, must have permissions to both CONNECT to the channel and have the MOVE_MEMBER permission. */
channelID?: string;
}
```
Rules:
- Camel Case
- Do not allow `null`
- Everything should have a comment explaining it
- These typings should be kept in the file with the function.
- Example: EditMemberOptions is at the bottom of the file where editMember()
is declared.
2. Discord Types Incoming
These types are meant for the payloads that we receive from Discord, whether
through gateway or REST.
Example:
```ts
export interface MemberCreatePayload {
/** The user this guild member represents */
user: UserPayload;
/** The user's guild nickname if one is set. */
nick?: string;
/** Array of role ids that the member has */
roles: string[];
/** When the user joined the guild. */
joined_at: string;
/** When the user used their nitro boost on the server. */
premium_since?: string;
/** Whether the user is deafened in voice channels */
deaf: boolean;
/** Whether the user is muted in voice channels */
mute: boolean;
/** Whether the user has passed the guild's Membership Screening requirements */
pending?: boolean;
}
```
Rules:
- Snake case (Or whatever discord uses. Everything here should be letter to
letter in accordance with discord's docs.)
- Everything should have a comment explaining it
- Kept in the src/types/api/incoming folder
3. Discord Types Outgoing
These types are meant **US** as we develop Discordeno. These will help us
prevent bugs when we are sending payloads to Discord whether through gateway or
REST.
Example:
```ts
export interface EditMemberPayload {
/** Value to set users nickname to. Requires MANAGE_NICKNAMES permission. */
nick?: string;
/** Array of role ids the member is assigned. Requires MANAGE_ROLES permission. */
roles?: string[];
/** Whether the user is muted in voice channels. Requires MUTE_MEMBERS permission. */
mute?: boolean;
/** Whether the user is deafened in voice channels. Requires DEAFEN_MEMBERS permission. */
deaf?: boolean;
/** The id of the channel to move user to if they are connected to voice. To kick the user from their current channel, set to null. Requires MOVE_MEMBERS permission. When moving members to channels, must have permissions to both CONNECT to the channel and have the MOVE_MEMBER permission. */
channel_id?: string | null;
}
```
Rules:
- Snake case (Or whatever discord uses. Everything here should be letter to
letter in accordance with discord's docs.)
- Everything should have a comment explaining it
- Kept in the src/types/api/outgoing folder
## Minimalistic
Since Discord uses `snake_case` but we use `camelCase` we will end up with
redundant typings. In order to solve this, we have the `Camelize<T>` type which
helps create a type using the snake case.
If we have the base payload for Discord's `User` as follows:
```ts
export interface DiscordUserPayload {
/** The user's id */
id: string;
/** the user's username, not unique across the platform */
username: string;
/** The user's 4 digit discord tag */
discriminator: string;
/** The user's avatar hash */
avatar: string | null;
/** Whether the user is a bot */
bot?: boolean;
/** Whether the user is an official discord system user (part of the urgent message system.) */
system?: boolean;
/** Whether the user has two factor enabled on their account */
"mfa_enabled"?: boolean;
/** the user's chosen language option */
locale?: string;
/** Whether the email on this account has been verified */
verified?: boolean;
/** The user's email */
email?: string;
/** The flags on a user's account. */
flags?: number;
/** The type of Nitro subscription on a user's account. */
premium_type?: number;
}
```
To create the Discordeno version for this it would be done as:
```ts
export interface UserPayload extends Camelize<DiscordUserPayload> {}
```
Now we have 2 unique interfaces, without having all the extra work or headaches
of maintaing 2 different interfaces.
Similarily, for any outgoing Discord types, we can also camelize them to have
better user experience for users.
Example:
```ts
export interface DiscordBanOptions {
/** number of days to delete messages for (0-7) */
delete_message_days?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
/** The reason for the ban. */
reason?: string;
}
```
To create the Discordeno version for this it would be done as:
```ts
export interface BanOptions extends Camelize<DiscordBanOptions> {}
```

View File

@@ -25,7 +25,7 @@ import {
PartialMessage,
ReactionPayload,
} from "./message.ts";
import { Camelize } from "./util.ts";
import { CamelCase } from "./util.ts";
export interface BotConfig {
token: string;
@@ -93,15 +93,15 @@ export interface EventHandlers {
rateLimit?: (data: RateLimitData) => unknown;
/** Sent when a new Slash Command is created, relevant to the current user. */
applicationCommandCreate?: (
data: Camelize<ApplicationCommandEvent>,
data: CamelCase<ApplicationCommandEvent>,
) => unknown;
/** Sent when a Slash Command relevant to the current user is updated. */
applicationCommandUpdate?: (
data: Camelize<ApplicationCommandEvent>,
data: CamelCase<ApplicationCommandEvent>,
) => unknown;
/** Sent when a Slash Command relevant to the current user is deleted. */
applicationCommandDelete?: (
data: Camelize<ApplicationCommandEvent>,
data: CamelCase<ApplicationCommandEvent>,
) => unknown;
/** Sent when properties about the user change. */
botUpdate?: (user: UserPayload) => unknown;
@@ -242,15 +242,19 @@ export interface EventHandlers {
/** Sent when a member has passed the guild's Membership Screening requirements */
membershipScreeningPassed?: (guild: Guild, member: Member) => unknown;
/** Sent when an integration is created on a server such as twitch, youtube etc.. */
integrationCreate?: (data: Camelize<IntegrationCreateUpdateEvent>) => unknown;
integrationCreate?: (
data: CamelCase<IntegrationCreateUpdateEvent>,
) => unknown;
/** Sent when an integration is updated. */
integrationUpdate?: (data: Camelize<IntegrationCreateUpdateEvent>) => unknown;
integrationUpdate?: (
data: CamelCase<IntegrationCreateUpdateEvent>,
) => unknown;
/** Sent when an integration is deleted. */
integrationDelete?: (data: Camelize<IntegrationDeleteEvent>) => undefined;
integrationDelete?: (data: CamelCase<IntegrationDeleteEvent>) => undefined;
/** Sent when a new invite to a channel is created. */
inviteCreate?: (data: Camelize<InviteCreateEvent>) => unknown;
inviteCreate?: (data: CamelCase<InviteCreateEvent>) => unknown;
/** Sent when an invite is deleted. */
inviteDelete?: (data: Camelize<InviteDeleteEvent>) => unknown;
inviteDelete?: (data: CamelCase<InviteDeleteEvent>) => unknown;
}
/** https://discord.com/developers/docs/topics/gateway#list-of-intents */

View File

@@ -1,10 +1,123 @@
export type CamelizeString<T extends PropertyKey> = T extends string
? string extends T ? string
: T extends `${infer F}_${infer R}`
? `${F}${T extends `${infer F}_id` ? Uppercase<R>
: Capitalize<CamelizeString<R>>}`
: T
: T;
type UpperCaseCharacters =
| "A"
| "B"
| "C"
| "D"
| "E"
| "F"
| "G"
| "H"
| "I"
| "J"
| "K"
| "L"
| "M"
| "N"
| "O"
| "P"
| "Q"
| "R"
| "S"
| "T"
| "U"
| "V"
| "W"
| "X"
| "Y"
| "Z";
// deno-fmt-ignore
export type Camelize<T> = { [K in keyof T as CamelizeString<K>]: T[K] };
type WordSeparators = "-" | "_" | " ";
type SplitIncludingDelimiters<
Source extends string,
Delimiter extends string,
> = Source extends "" ? []
: Source extends `${infer FirstPart}${Delimiter}${infer SecondPart}` ? (
Source extends `${FirstPart}${infer UsedDelimiter}${SecondPart}`
? UsedDelimiter extends Delimiter
? Source extends `${infer FirstPart}${UsedDelimiter}${infer SecondPart}`
? [
...SplitIncludingDelimiters<FirstPart, Delimiter>,
UsedDelimiter,
...SplitIncludingDelimiters<SecondPart, Delimiter>,
]
: never
: never
: never
)
: [Source];
type StringPartToDelimiterCase<
StringPart extends string,
UsedWordSeparators extends string,
UsedUpperCaseCharacters extends string,
Delimiter extends string,
> = StringPart extends UsedWordSeparators ? Delimiter
: StringPart extends UsedUpperCaseCharacters
? `${Delimiter}${Lowercase<StringPart>}`
: StringPart;
type StringArrayToDelimiterCase<
Parts extends any[],
UsedWordSeparators extends string,
UsedUpperCaseCharacters extends string,
Delimiter extends string,
> = Parts extends [`${infer FirstPart}`, ...infer RemainingParts]
? `${StringPartToDelimiterCase<
FirstPart,
UsedWordSeparators,
UsedUpperCaseCharacters,
Delimiter
>}${StringArrayToDelimiterCase<
RemainingParts,
UsedWordSeparators,
UsedUpperCaseCharacters,
Delimiter
>}`
: "";
type DelimiterCase<Value, Delimiter extends string> = Value extends string
? StringArrayToDelimiterCase<
SplitIncludingDelimiters<Value, WordSeparators | UpperCaseCharacters>,
WordSeparators,
UpperCaseCharacters,
Delimiter
>
: Value;
type InnerCamelCaseStringArray<Parts extends any[], PreviousPart> =
Parts extends [`${infer FirstPart}`, ...infer RemainingParts]
? FirstPart extends undefined ? ""
: FirstPart extends ""
? InnerCamelCaseStringArray<RemainingParts, PreviousPart>
: `${PreviousPart extends "" ? FirstPart
: Capitalize<FirstPart>}${InnerCamelCaseStringArray<
RemainingParts,
FirstPart
>}`
: "";
type CamelCaseStringArray<Parts extends string[]> = Parts extends
[`${infer FirstPart}`, ...infer RemainingParts] ? Uncapitalize<
`${FirstPart}${InnerCamelCaseStringArray<RemainingParts, FirstPart>}`
>
: never;
type Split<S extends string, D extends string> = string extends S ? string[]
: S extends "" ? []
: S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>]
: [S];
export type SnakeCase<Value> = DelimiterCase<Value, "_">;
export type CamelCase<K> = K extends string
? CamelCaseStringArray<Split<K, WordSeparators>>
: K;
export type CamelCasedProps<T> = {
[K in keyof T as CamelCase<K>]: T[K];
};
export type SnakeCasedProps<T> = {
[K in keyof T as SnakeCase<K>]: T[K];
};