When code has multiple behavioral variants (output format, hook context type, session state), prefer modeling the variation explicitly with types/roles and reusing shared logic—rather than duplicating implementations or threading many conditionals/flags.
Apply these rules:
Example pattern (roles + straight-line lifecycle):
type OutputMode = "json" | "console";
type RunOutputter = {
runStarted(): void;
runResult(result: unknown): void;
runAborted(reason: unknown): void;
verbose: boolean;
};
function createRunOutputter(mode: OutputMode, verbose: boolean): RunOutputter {
if (mode === "json") return new JsonRunOutputter({ verbose });
return new ConsoleRunOutputter({ verbose });
}
export async function runAgent(/*...*/ config: { outputMode: OutputMode; verbose: boolean }) {
const outputter = createRunOutputter(config.outputMode, config.verbose);
outputter.runStarted();
try {
const result = await doRun();
outputter.runResult(result);
} catch (e) {
outputter.runAborted(e);
}
}
Example pattern (union instead of boolean):
type ResumeState = "none" | "resumed";
type HookRuntimeOptions = {
resumed?: ResumeState; // better than boolean
};
Checklist: if you’re seeing repeated formatting wrappers, copied normalization code, nested if (outputMode === "json") ... branches, or *?: boolean options representing more than two real states—refactor toward shared utilities, role-based dispatch, and discriminated unions.