One key aspect to consider is that your CustomCommand
already encompasses the CoreCommand
within its expression as CoreCommand | Replace
equates to Insert | Delete | Replace
. It seems there is no immediate need for polymorphism in this scenario; you can simply utilize the CustomCommand
:
const exec = (command: CustomCommand) => {
switch (command.name) { // Command name is properly insert | delete | replace
case 'insert':
// ...
break
case 'delete':
// ...
break
// ...
}
Another consideration is how to ensure that any newly created commands adhere to the interface of the Command
. This can be achieved through conditional types that verify if the name does not already exist in CoreCommands
:
type CreateCommand<T extends Command> = T['name'] extends CoreCommand['name'] ? never : T;
type Insert2 = CreateCommand<{ name: 'insert'; position: number; text: string }>
type CustomCommand = CoreCommand | Insert2 // Resulting in Insert | Delete only
The use of CreateCommand
will result in "never" if a type with a similar name property already exists in core commands. Thus, types sharing the same name field will be omitted from the union.
You could also create an additional utility type for creating a union that always incorporates CoreCommands:
type Replace = CreateCommand<{ name: 'replace'; position: number; text: string }>
type Change = CreateCommand<{ name: 'change'; position: number; text: string }>
type MakeCommands<NewCommand extends Command> = CoreCommand | NewCommand
type CustomCommand = MakeCommands<Replace | Change>; // Ensuring CoreCommand is always included
Finally, let's discuss making the exec function polymorphic:
const exec = <C extends Command>(command: C, f: (x: C) => void) => f(command);
const comm = { name: 'replace', position: 1, text: 'text' };
// The 'as' statement here blocks direct inference by TS from const, it is unnecessary in real code
const result = exec(comm as CustomCommand, (incommand) => {
switch (incommand.name) { // Incommand refers to CustomCommand here
case 'insert':
// ...
break
case 'delete':
// ...
break
// ...
}
});
In the above implementation, exec
infers the type from the first argument and passes the inferred type to the function as the second argument. This approach ensures a specified type containing CoreCommands and any command included in the type provided as the command
argument.