#Complex type parameter dependent interfaces/classes

14 messages · Page 1 of 1 (latest)

wise ravine
#

Discord now has three distinct types of application commands: chat input (slash commands), message (right click message context menu), and user (right click user context menu). These 3 different types are part of an enum: ApplicationCommandType. My goal is to implement a class where I can say new Command<T>(CommandProperties<T>) where T is one of the 3 types, and what happens inside the class depends on which type T is. I also have several interfaces giving the possible constructor arguments for each type. So far what I have looks like this:

export class Command<T extends ApplicationCommandType> {
  constructor(properties: CommandProperties<T>) {

  }
}

export type CommandProperties<T extends ApplicationCommandType> =
  T extends ApplicationCommandType.ChatInput
    ? SlashCommandProperties
    : T extends ApplicationCommandType.Message
    ? MessageContextMenuCommandProperties
    : UserContextMenuCommandProperties;

CommandProperties takes a command type and finds the right properties (arguments) for that type. So far, this works for creating new Commands from the outside with arguments that correspond to the command type T.
However I don't know how (or if it's possible) to change how the Command class works (assigning properties, different methods, etc) depending on T. Possibly what I'm looking for is a way to "overload" the class differentiated by type parameter as opposed to arguments(?)
The final goal is a class and types that work such that I can use new Command<T>(CommandProperties<T>) to create a new command of one of the 3 types that functions uniquely as that specific type.

if anyones got ideas, it'd be much appreciated, thanks:3

wise ravine
#

Here's a playground link with some more code and some usage:

white turretBOT
#
MrSmarty#1732

Preview:```ts
// THE command class
class Command<T extends ApplicationCommandType> {

// @ts-ignore
data: SlashCommandBuilder | ContextMenuCommandBuilder; // needs to vary depending on T
// @ts-ignore
name: string; 
// @ts-ignore
permission: string;

...```

odd matrix
#

First of I would move the type from the generic:

new Command<ApplicationCommandType.ChatInput>({
    name: "slash", 

into its own type field

new Command({
    type: ApplicationCommandType.ChatInput,
    name: "slash", 

you can infer this for the TS types, and you can use it now in the constrctor to run different logic depending on the type. (maybe in a switch)

#

next instead of creating a completely new type for each UserContextMenuCommandProperties
, create a base type and extend on it, something like this:

interface BaseCommand {
  type: ApplicationCommandType;
  name: string;
  // ...
}

interface ChatInputCommand extends BaseCommand {
  type: ApplicationCommandType.ChatInput;
  // ...
}

interface UserCommand extends BaseCommand {
  type: ApplicationCommandType.User;
  // ...
}

or if you dont want to use extend, this will do the same thing but will be less type safe:

interface ChatInputCommand {
  type: ApplicationCommandType.ChatInput;
  // ...
}

interface UserCommand {
  type: ApplicationCommandType.User;
  // ...
}

type BaseCommand = ChatInputCommand | UserCommand;
#
white turretBOT
#

@odd matrix Here's a shortened URL of your playground link! You can remove the full link from your message.

bmkotu#7304

Preview:```ts
// ApplicationCommandType
enum ApplicationCommandType {
ChatInput = 1,
User = 2
}

type ChatInputCommandBuilder = "slashBuilder";
type UserCommandBuilder = "contextBuilder";

interface BaseCommand {
type: ApplicationCommandType;
name: string;
// ...
...```

odd matrix
#

I guess you could also overload the constructor, that would look something like this:

class Command<T extends ApplicationCommandType> {
  constructor(properties: SlashCommand);
  constructor(properties: MessageContextMenuCommand);
  constructor(properties: UserContextMenuCommand);
  constructor(properties: CommandProperties<T>) {

but I think its harder to then infer stuf

wise ravine
#

Thank you so much! going to try and implement this now

wise ravine
#

@odd matrix i tried implementing your solution while keeping some field declarations on the class level
ex. having name, and permission on the same level as the data field and keeping the data field reserved for the builder

export class Command<T extends BaseCommand> {
    type: ApplicationCommandType;
    data: InferCommandBuilder<T>;
    name: string;
    permission: RoleIdentifier;
    // ...
}

this takes away the convenience of being able to completely swap out the field declarations by using a single data field but makes it so you don't have to use data. to access every single property. I created a few more Infer types to map the command type to various related classes. I used the if-else chain inside the constructor with typeguards changing the BaseCommand extended arguments to a specific command type interface. however, im running into some new issues. When i try to set the this.data field to the correct builder, I get this error

odd matrix
#

hard to say what the issue is without more context, but I would assume from just this that it has something to do with the missing <T>, if its really just TS that is not able to handle how you set this property, then you could just to this.data = x as typeof this.data

wise ravine
#

Hmm.. doesnt fix it because this.data can be a few different things, and not every method/prop exists on everything this.data can be

wise ravine