Skip to content

Instantly share code, notes, and snippets.

@malef
Last active September 3, 2019 19:59
Show Gist options
  • Save malef/fd582052a5b36b4eaf6aa06bc2cb924e to your computer and use it in GitHub Desktop.
Save malef/fd582052a5b36b4eaf6aa06bc2cb924e to your computer and use it in GitHub Desktop.
Draft of command executor using generics and advanced types.
// ----- Logger (mostly irrelevant for this case).
interface LoggerInterface {
info(message: string): void;
}
class Logger implements LoggerInterface {
info(message: string): void {
console.log(message);
}
}
// ----- Command kinds defined as enum so that I don't need to repeat strings.
enum CommandKind {
first = 'first',
second = 'second',
}
// ----- Command payloads (input) and results (output) defined.
type CommandPayload<T extends CommandKind> =
T extends CommandKind.first ? FirstCommandPayload :
T extends CommandKind.second ? SecondCommandPayload :
never;
type CommandResult<T extends CommandKind> =
T extends CommandKind.first ? FirstCommandResult :
T extends CommandKind.second ? SecondCommandResult :
never;
type FirstCommandPayload = {
readonly source: string;
readonly foo: number;
readonly bar: string;
};
type FirstCommandResult = {
readonly foo: number;
readonly bar: string;
};
type SecondCommandPayload = {
readonly source: string;
readonly baz: string;
};
type SecondCommandResult = {
readonly baz: string,
};
// ----- Command defined wrapping kind, payload and result.
type Command<T extends CommandKind> = {
kind: T;
payload: CommandPayload<T>;
result?: CommandResult<T>;
}
// ---- Command handlers with interface to ensure that main method is defined properly.
type CommandHandler<T extends CommandKind> =
T extends CommandKind.first ? FirstCommandHandler :
T extends CommandKind.second ? SecondCommandHandler :
never;
interface CommandHandlerInterface<T extends CommandKind> {
handleCommand(
commandPayload: CommandPayload<T>,
logger: LoggerInterface
): Promise<CommandResult<T>>;
}
class FirstCommandHandler implements CommandHandlerInterface<CommandKind.first> {
async handleCommand(
commandPayload: CommandPayload<CommandKind.first>,
logger: LoggerInterface
): Promise<CommandResult<CommandKind.first>> {
logger.info(`executing first`);
const commandResult: FirstCommandResult = {
foo: commandPayload.foo * 10,
bar: commandPayload.bar + '!',
};
return commandResult;
}
}
class SecondCommandHandler implements CommandHandlerInterface<CommandKind.second> {
async handleCommand(
commandPayload: CommandPayload<CommandKind.second>,
logger: LoggerInterface
): Promise<CommandResult<CommandKind.second>> {
logger.info('executing second');
const commandResult: SecondCommandResult = {
baz: commandPayload.baz + '!',
};
return commandResult;
}
}
// ----- Map used to match handlers with commands using kind.
type CommandHandlersMap = {
[commandKind in CommandKind]: CommandHandler<commandKind>;
};
// ----- Handler able to handle any registered command(s).
class CompositeCommandHandler {
constructor(
readonly commandHandlersMap: CommandHandlersMap,
) {}
async handleOne(command: Command<CommandKind>): Promise<Command<CommandKind>> {
const logger = new Logger();
const commandKind = command.kind;
const commandHandler = this.commandHandlersMap[commandKind];
// I was hoping this will work, but it highlights command.payload as not matching
// FirstCommandPayload & SecondCommandPayload which probably means it cannot determine
// handler type properly.
//
// if (
// this.commandTypeGuard(command, commandKind)
// && this.commandHandlerTypeGuard(commandHandler, commandKind)
// ) {
// command.result = await commandHandler.handleCommand(command.payload, logger);
// }
//
// Therefore I have to do it like below, using a list of if statements which doesn't look nice.
if (
this.commandTypeGuard(command, CommandKind.first)
&& this.commandHandlerTypeGuard(commandHandler, CommandKind.first)
) {
command.result = await commandHandler.handleCommand(command.payload, logger);
} else if (
this.commandTypeGuard(command, CommandKind.second)
&& this.commandHandlerTypeGuard(commandHandler, CommandKind.second)
) {
command.result = await commandHandler.handleCommand(command.payload, logger);
} else {
throw new Error('Command handler not found');
}
return command;
}
async handleMany(commands: Command<CommandKind>[]): Promise<Command<CommandKind>[]> {
const results: Command<CommandKind>[] = [];
for (const command of commands) {
results.push(
await this.handleOne(command),
);
}
return results;
}
private commandTypeGuard<T extends CommandKind>(
command: Command<CommandKind>,
commandKind: T,
): command is Command<T> {
return command.kind === commandKind;
}
private commandHandlerTypeGuard<T extends CommandKind>(
commandHandler: CommandHandler<CommandKind>,
commandKind: T,
): commandHandler is CommandHandler<T> {
return this.commandHandlersMap[commandKind] === commandHandler;
}
}
// ----- Putting it all together and executing some commands.
const compositeCommandHandler = new CompositeCommandHandler(
{
[CommandKind.first]: new FirstCommandHandler(),
[CommandKind.second]: new SecondCommandHandler(),
}
);
const commands: Command<CommandKind>[] = [
// What can I do to avoid casting these objects to <Command<CommandKind.first>>?
// Can type be somehow determined by kind property being checked automatically?
<Command<CommandKind.first>> {
kind: CommandKind.first,
payload: {
source: 'repo-1',
foo: 7,
bar: 'bar-1',
},
},
<Command<CommandKind.first>> {
kind: CommandKind.first,
payload: {
source: 'repo-2',
foo: 3,
bar: 'bar-2',
},
},
<Command<CommandKind.second>> {
kind: CommandKind.second,
payload: {
source: 'repo-3',
baz: 'baz-1',
},
},
<Command<CommandKind.first>> {
kind: CommandKind.first,
payload: {
source: 'repo-3',
foo: 5,
bar: 'bar-3',
},
},
];
console.log();
console.log('Execution started.');
console.log();
compositeCommandHandler
.handleMany(commands)
.then(
results => {
console.log();
console.log('Execution completed.');
console.log();
console.log(results);
console.log();
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment