Skip to content

Commit f651afa

Browse files
feat: add support for autocomplete interactions
1 parent 8b97658 commit f651afa

File tree

4 files changed

+145
-10
lines changed

4 files changed

+145
-10
lines changed

src/data.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ type CommandOptionNumericData = Pick<ApplicationCommandNumericOption, 'name' | '
4545
type CommandOptionStringData = Pick<ApplicationCommandStringOption, 'name' | 'type' | 'required'> & {
4646
choices?: ReadonlyArray<ApplicationCommandOptionChoiceData<string>>;
4747
};
48+
49+
type AutocompleteOptionNumericData = Pick<ApplicationCommandNumericOption, 'name' | 'type' | 'required'> & {
50+
autocomplete: true;
51+
};
52+
53+
type AutocompleteOptionStringData = Pick<ApplicationCommandStringOption, 'name' | 'type' | 'required'> & {
54+
autocomplete: true;
55+
};
56+
4857
type CommandOptionChannelData = Pick<ApplicationCommandChannelOption, 'name' | 'type' | 'required'>;
4958
type CommandOptionRoleData = Pick<ApplicationCommandRoleOption, 'name' | 'type' | 'required'>;
5059
type CommandOptionUserData = Pick<ApplicationCommandUserOption, 'name' | 'type' | 'required'>;
@@ -63,6 +72,8 @@ export type CommandOptionBasicData =
6372
| CommandOptionAttachmentOptionData;
6473

6574
export type CommandOptionAnyData = CommandOptionBasicData | CommandOptionSubcommandData | CommandOptionSubcommandGroupData;
75+
export type CommandOptionAutocompleteData = AutocompleteOptionNumericData | AutocompleteOptionStringData;
76+
export type CommandOptionSubcommandOrGroupData = CommandOptionSubcommandData | CommandOptionSubcommandGroupData;
6677

6778
export type CommandBaseData = Pick<ChatInputApplicationCommandData, 'name'> & {
6879
options?: ReadonlyArray<CommandOptionAnyData>;

src/lib.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
2-
ChatInputCommandInteraction
2+
ChatInputCommandInteraction,
3+
AutocompleteInteraction
34
} from 'discord.js';
45

56
import type {
@@ -11,13 +12,16 @@ import type {
1112
BaseCommand,
1213
BaseCommandList,
1314
CommandWithOptions,
15+
CommandHasSubcommands,
1416
TypedCommandOptionsResolver,
1517
TypedCommandOptionsNeverResolver,
18+
TypedAutocompleteOptionsResolver,
19+
TypedAutocompleteOptionsNeverResolver,
1620
PickBaseCommandByName,
1721
ExtractCommandSubcommands,
1822
ExtractCommandBasicOptions,
1923
ExtractSubcommandBasicOptions,
20-
CommandHasSubcommands
24+
AutocompleteCommands
2125
} from './index.js';
2226

2327
export declare class TypedCommandOptions<
@@ -38,6 +42,24 @@ export declare class TypedCommandSubcommandOptions<
3842
public options: TypedCommandOptionsResolver<ExtractSubcommandBasicOptions<CommandWithOptions<T>['options'], K>>;
3943
}
4044

45+
export declare class TypedAutocompleteCommandOptions<
46+
T extends BaseCommand
47+
> extends AutocompleteInteraction<CacheType> {
48+
public commandName: T['name'];
49+
public options: CommandHasSubcommands<CommandWithOptions<T>['options']> extends true
50+
? TypedAutocompleteOptionsNeverResolver<ExtractCommandBasicOptions<CommandWithOptions<T>['options']>>
51+
: TypedAutocompleteOptionsResolver<ExtractCommandBasicOptions<CommandWithOptions<T>['options']>>;
52+
}
53+
54+
export declare class TypedAutocompleteSubcommandOptions<
55+
T extends BaseCommand,
56+
K extends ExtractCommandSubcommands<CommandWithOptions<T>['options']>['name']
57+
> extends AutocompleteInteraction<CacheType> {
58+
private subcommandName: K;
59+
public commandName: T['name'];
60+
public options: TypedAutocompleteOptionsResolver<ExtractSubcommandBasicOptions<CommandWithOptions<T>['options'], K>>;
61+
}
62+
4163
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4264
export function typed<T extends BaseCommandList = BaseCommandList>(commandList: T) {
4365
function command<
@@ -55,9 +77,28 @@ export function typed<T extends BaseCommandList = BaseCommandList>(commandList:
5577
return (interaction as TypedSubcommand<T, K, S>).options.getSubcommand() === name;
5678
}
5779

80+
function autocomplete<
81+
A extends AutocompleteCommands<T>,
82+
K extends A[number]['name']
83+
>
84+
(interaction: Interaction, name: K): interaction is TypedAutocompleteCommand<A, K> {
85+
return interaction.isAutocomplete() && interaction.commandName === name;
86+
}
87+
88+
function autocompleteSubcommand<
89+
A extends AutocompleteCommands<T>,
90+
K extends A[number]['name'],
91+
S extends ExtractCommandSubcommands<CommandWithOptions<PickBaseCommandByName<A, K>>['options']>['name']
92+
>
93+
(interaction: TypedAutocompleteCommand<A, K>, name: S): interaction is TypedAutocompleteSubcommand<A, K, S> {
94+
return (interaction as TypedAutocompleteSubcommand<A, K, S>).options.getSubcommand() === name;
95+
}
96+
5897
return {
5998
command,
60-
subcommand
99+
subcommand,
100+
autocomplete,
101+
autocompleteSubcommand
61102
};
62103
}
63104

@@ -71,3 +112,14 @@ export type TypedSubcommand<
71112
K extends T[number]['name'],
72113
S extends ExtractCommandSubcommands<CommandWithOptions<PickBaseCommandByName<T, K>>['options']>['name']
73114
> = TypedCommandSubcommandOptions<PickBaseCommandByName<T, K>, S>;
115+
116+
export type TypedAutocompleteCommand<
117+
T extends BaseCommandList,
118+
K extends T[number]['name']
119+
> = TypedAutocompleteCommandOptions<PickBaseCommandByName<T, K>>;
120+
121+
export type TypedAutocompleteSubcommand<
122+
T extends BaseCommandList,
123+
K extends T[number]['name'],
124+
S extends ExtractCommandSubcommands<CommandWithOptions<PickBaseCommandByName<T, K>>['options']>['name']
125+
> = TypedAutocompleteSubcommandOptions<PickBaseCommandByName<T, K>, S>;

src/resolver.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import type {
1111
CommandOptionBasicDataType,
1212
CommandOptionChoiceDataType,
1313
PickCommandOptionByName,
14+
AutocompleteCommands,
15+
ExtractAutocompleteOption,
16+
CommandOptionAutocompleteData
1417
} from './index.js';
1518

1619
type BaseData<T extends CommandOptionBasicData> = { name: T['name'], type: T['type'] };
@@ -91,17 +94,47 @@ type CommandDataMapper<T extends CommandOptionBasicData> =
9194
: T['type'] extends ApplicationCommandOptionType.SubcommandGroup ? BaseData<T>
9295
: never;
9396

97+
/**
98+
* @note T extends T does not work here.
99+
*/
100+
type AutocompleteFocusedMapper<T extends CommandOptionAutocompleteData> = T extends any ? {
101+
name: T['name'];
102+
type: T['type'];
103+
value: string;
104+
focused: true;
105+
} : never;
106+
107+
type AutocompleteDataMapper<T extends CommandOptionBasicData> = {
108+
name: T['name'];
109+
type: T['type'];
110+
value: string;
111+
focused?: true;
112+
};
113+
94114
export interface TypedCommandOptionsResolver<T extends CommandOptionBasicData> extends Omit<CommandInteractionOptionResolver<CacheType>, 'getMessage' | 'getFocused'> {
95115
get<K extends T['name']>(name: K): NullableData<PickCommandOptionByName<T, K>>;
96116
get<K extends T['name']>(name: K, required: true): CommandDataMapper<PickCommandOptionByName<T, K>>;
97117
get<K extends T['name']>(name: K, required?: boolean): CommandDataMapper<PickCommandOptionByName<T, K>>;
98118
}
99119

100120
export interface TypedCommandOptionsNeverResolver<T extends CommandOptionBasicData> extends Omit<CommandInteractionOptionResolver<CacheType>, 'getMessage' | 'getFocused'> {
101-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
102-
get<K extends T['name']>(name: never): CommandInteractionOption<CacheType>;
103-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
104-
get<K extends T['name']>(name: never, required: never): CommandInteractionOption<CacheType>;
105-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
106-
get<K extends T['name']>(name: never, required?: never): CommandInteractionOption<CacheType>;
121+
get<K extends T['name']>(name: never): NullableData<PickCommandOptionByName<T, K>>;
122+
get<K extends T['name']>(name: never, required: never): CommandDataMapper<PickCommandOptionByName<T, K>>;
123+
get<K extends T['name']>(name: never, required?: never): CommandDataMapper<PickCommandOptionByName<T, K>>;
124+
}
125+
126+
export interface TypedAutocompleteOptionsResolver<T extends CommandOptionBasicData> extends Omit<CommandInteractionOptionResolver<CacheType>, 'getMessage' | 'getUser' | 'getAttachment' | 'getChannel' | 'getMember' | 'getMentionable' | 'getRole'> {
127+
get<K extends T['name']>(name: K): AutocompleteDataMapper<PickCommandOptionByName<T, K>> | null;
128+
get<K extends T['name']>(name: K, required: true): AutocompleteDataMapper<PickCommandOptionByName<T, K>>;
129+
get<K extends T['name']>(name: K, required?: boolean): AutocompleteDataMapper<PickCommandOptionByName<T, K>> | null;
130+
getFocused(getFull: true): AutocompleteFocusedMapper<ExtractAutocompleteOption<T>>;
131+
getFocused(getFull?: boolean): string;
132+
}
133+
134+
export interface TypedAutocompleteOptionsNeverResolver<T extends CommandOptionBasicData> extends Omit<CommandInteractionOptionResolver<CacheType>, 'getMessage' | 'getUser' | 'getAttachment' | 'getChannel' | 'getMember' | 'getMentionable' | 'getRole'> {
135+
get<K extends T['name']>(name: never): AutocompleteDataMapper<PickCommandOptionByName<T, K>> | null;
136+
get<K extends T['name']>(name: never, required: never): AutocompleteDataMapper<PickCommandOptionByName<T, K>>;
137+
get<K extends T['name']>(name: never, required?: never): AutocompleteDataMapper<PickCommandOptionByName<T, K>> | null;
138+
getFocused(getFull: never): AutocompleteFocusedMapper<ExtractAutocompleteOption<T>>;
139+
getFocused(getFull?: never): string;
107140
}

src/utility.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,23 @@ import type {
44
BaseCommand,
55
BaseCommandList,
66
CommandOptionAnyData,
7+
CommandOptionAutocompleteData,
78
CommandOptionBasicData,
89
CommandOptionSubcommandData,
10+
CommandOptionSubcommandOrGroupData,
11+
TypedAutocompleteCommand,
912
TypedCommand
1013
} from './index.js';
1114

15+
/**
16+
* For dev/debugging purposes
17+
* @see https://stackoverflow.com/a/57683652/3258251
18+
*/
19+
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
20+
export type ExpandRecursively<T> = T extends object
21+
? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
22+
: T;
23+
1224
/**
1325
* Deeply nested immutable (readonly) array object remapper:
1426
* @see https://github.com/microsoft/TypeScript/issues/13923#issuecomment-557509399
@@ -29,6 +41,10 @@ export type TypedCommandList<T extends BaseCommandList> = {
2941
[K in T[number]['name']]: TypedCommand<T, K>
3042
};
3143

44+
export type TypedAutocompleteList<T extends BaseCommandList, A extends AutocompleteCommands<T> = AutocompleteCommands<T>> = {
45+
[K in A[number]['name']]: TypedAutocompleteCommand<A, K>
46+
};
47+
3248
/**
3349
* Make a property of an object required. Works well with intersections.
3450
* @see https://bobbyhadz.com/blog/typescript-make-property-required#make-an-optional-property-required-in-typescript
@@ -64,6 +80,8 @@ export type PickCommandOptionByName<T extends CommandOptionBasicData, K extends
6480
: never;
6581

6682
type ExtractBasicOption<T extends CommandOptionAnyData> = T extends CommandOptionBasicData ? T : never;
83+
export type ExtractAutocompleteOption<T extends CommandOptionAnyData> = T extends CommandOptionAutocompleteData ? T : never;
84+
6785
type ExtractSubcommand<T extends CommandOptionAnyData> = T extends CommandOptionSubcommandData ? T : never;
6886

6987
export type ExtractCommandBasicOptions<T extends ReadonlyArray<CommandOptionAnyData>> =
@@ -87,4 +105,25 @@ export type ExtractSubcommandBasicOptions<T extends ReadonlyArray<CommandOptionA
87105
: Options extends { options: ReadonlyArray<CommandOptionAnyData> }
88106
? ExtractSubcommandBasicOptions<Options['options'], K>
89107
: never
90-
: never;
108+
: never;
109+
110+
type HasAutocompleteData<T extends ReadonlyArray<CommandOptionAnyData>> = {
111+
[K in keyof T]: T[K] extends { autocomplete: true } ? T[K] : never;
112+
} extends ReadonlyArray<never> ? false : true;
113+
114+
type FilterNeverOptions<T> = T extends { options: ReadonlyArray<never> } ? never : T;
115+
116+
type FilterAutocompleteOptions<T> =
117+
T extends { options: ReadonlyArray<CommandOptionBasicData> }
118+
? HasAutocompleteData<T['options']> extends true
119+
? T
120+
: never
121+
: T extends { options: ReadonlyArray<CommandOptionAnyData> }
122+
? T extends { options: infer Options extends ReadonlyArray<CommandOptionSubcommandOrGroupData> }
123+
? FilterNeverOptions<Omit<T, 'options'> & { options: AutocompleteCommands<Options> }>
124+
: never
125+
: never;
126+
127+
export type AutocompleteCommands<T extends BaseCommandList> = {
128+
[K in keyof T]: FilterAutocompleteOptions<T[K]>
129+
};

0 commit comments

Comments
 (0)