all repos — kitten @ main

pkgs/router/src/options.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
import {
	type AutocompleteInteraction,
	type User,
	type Attachment,
	type Role,
	type GuildBasedChannel,
	type ApplicationCommandOptionChoiceData,
	type LocalizationMap,
} from "discord.js";

export const OPTION_TYPES = {
	subcommand: 1,
	subcommandGroup: 2,
	string: 3,
	integer: 4,
	boolean: 5,
	user: 6,
	channel: 7,
	role: 8,
	mentionable: 9,
	number: 10,
	attachment: 11,
} as const;
export type OptionType = keyof typeof OPTION_TYPES;

export interface OptionChoice<T> {
	name: string;
	nameLocalizations?: LocalizationMap;
	value: T;
}

export interface OptionConfig<T> {
	required?: boolean;
	autocomplete?: AutocompleteFn<T>;
	choices?: OptionChoice<T>[];
	nameLocalizations?: LocalizationMap;
	descriptionLocalizations?: LocalizationMap;
}

export interface Option<T, Req extends boolean> extends OptionConfig<T> {
	type: OptionType;
	description?: string;
	required: Req;
	__type?: T;
}

export interface CommandOption<T, Req extends boolean> extends Option<T, Req> {
	description: string;
}

/**
 * recursively infers strict typescript types from your option schema.
 * options marked as `required: true` will be inferred as `T`, while optional
 * parameters will be inferred as `T | undefined`.
 */
export type InferOptions<O> = {
	[K in keyof O]: O[K] extends Option<infer Type, infer Required>
		? Required extends true
			? Type
			: Type | undefined
		: never;
};

/**
 * a helper function to provide dynamic autocomplete choices to the user.
 * kitten handles routing the autocomplete interaction and running this helper automatically.
 */
export type AutocompleteFn<T> = (
	interaction: AutocompleteInteraction,
	value: T,
) => Promise<ApplicationCommandOptionChoiceData[]> | ApplicationCommandOptionChoiceData[];

export interface OptionBuilder<T> {
	(description: string, config: OptionConfig<T> & { required: true }): CommandOption<T, true>;
	(description: string, config?: OptionConfig<T> & { required?: false }): CommandOption<T, false>;
	(config: OptionConfig<T> & { required: true }): Option<T, true>;
	(config?: OptionConfig<T> & { required?: false }): Option<T, false>;

	required(description: string, config?: Omit<OptionConfig<T>, "required">): CommandOption<T, true>;
	optional(
		description: string,
		config?: Omit<OptionConfig<T>, "required">,
	): CommandOption<T, false>;
}

const createOption = <T>(type: OptionType): OptionBuilder<T> => {
	const build = (
		descOrConfig?: string | OptionConfig<T>,
		configObj?: OptionConfig<T>,
		overrideReq?: boolean,
	): any => {
		const hasDesc = typeof descOrConfig === "string";
		const description = hasDesc ? descOrConfig : undefined;
		const actualConfig = hasDesc ? configObj : (descOrConfig as OptionConfig<T>);

		return {
			type,
			description,
			required: overrideReq ?? actualConfig?.required ?? false,
			autocomplete: actualConfig?.autocomplete,
			choices: actualConfig?.choices,
			nameLocalizations: actualConfig?.nameLocalizations,
			descriptionLocalizations: actualConfig?.descriptionLocalizations,
		};
	};

	const fn: any = (doc?: any, c?: any) => build(doc, c);
	fn.required = (d: string, c?: any) => build(d, c, true);
	fn.optional = (d: string, c?: any) => build(d, c, false);

	return fn as OptionBuilder<T>;
};

/**
 * definitions for command and component options. these generate type-safe parameters
 * directly inside of your interaction's `run` callback.
 *
 * @example
 * ```ts
 * options: {
 * 	user: option.user.required("the user to get information about"),
 * 	ephemeral: option.boolean.optional("whether the response should be ephemeral"),
 * }
 * ```
 */
export const option = {
	string: createOption<string>("string"),
	integer: createOption<number>("integer"),
	number: createOption<number>("number"),
	boolean: createOption<boolean>("boolean"),
	user: createOption<User>("user"),
	channel: createOption<GuildBasedChannel>("channel"),
	role: createOption<Role>("role"),
	mentionable: createOption<User | Role>("mentionable"),
	attachment: createOption<Attachment>("attachment"),
};