+TypeScript example
The base class owns trimming, empty-input validation and the prefix; format() holds the representation-specific step. Formats can change without copying and potentially omitting common checks.
type ExportResult = { success: true; text: string } | { success: false; code: "EMPTY_INPUT" };
// Keep the shared sequence and validation rules in the base class.
abstract class ReportTemplate {
run(value: string): ExportResult {
const clean = value.trim();
if (clean.length === 0) return { success: false, code: "EMPTY_INPUT" };
return { success: true, text: "REPORT|" + this.format(clean) };
}
protected abstract format(value: string): string;
}
// Subclasses change this formatting step rather than the whole workflow.
class UpperReport extends ReportTemplate {
protected override format(value: string): string { return value.toUpperCase(); }
}
class JsonReport extends ReportTemplate {
protected override format(value: string): string { return JSON.stringify(value); }
}
for (const report of [new UpperReport(), new JsonReport()]) {
const result = report.run(" hello ");
console.log(result.success ? result.text : result.code);
}
const empty = new UpperReport().run(" ");
console.log(empty.success ? empty.text : empty.code);
REPORT|HELLO
REPORT|"hello"
EMPTY_INPUT
TypeScript has no final restriction equivalent to the non-virtual C# method; these subclasses implement format() without overriding run(). Empty input returns a failure before formatting is called. These comparisons use ASCII input; Unicode whitespace and case conversion can differ between the runtimes.
Compose functions and LINQ instead of inheritance
A function parameter is simpler when only one string transformation varies. Run still owns trimming and empty-input validation, while the caller supplies the transformation. LINQ Select applies the same workflow to each input.
C#
using System;
using System.Linq;
using System.Text.Json;
ExportResult Run(string input, Func<string, string> format)
{
string value = input.Trim();
if (value.Length == 0) return new ExportResult(false, "EMPTY_INPUT");
return new ExportResult(true, "REPORT|" + format(value));
}
var inputs = new[] { " hello ", " " };
// Supply only the format; invalid input fails before the transformation is called.
var results = inputs.Select(input => Run(input, value => JsonSerializer.Serialize(value)))
.ToArray();
foreach (ExportResult result in results) Console.WriteLine(result.Text);
sealed record ExportResult(bool Success, string Text);
TypeScript
type ExportResult = { success: true; text: string } | { success: false; code: "EMPTY_INPUT" };
function run(input: string, format: (value: string) => string): ExportResult {
const value = input.trim();
if (value.length === 0) return { success: false, code: "EMPTY_INPUT" };
return { success: true, text: "REPORT|" + format(value) };
}
const inputs = [" hello ", " "];
// Keep the shared workflow in one function and apply it to every input.
const results = inputs.map(input => run(input, value => JSON.stringify(value)));
for (const result of results) console.log(result.success ? result.text : result.code);
REPORT|"hello"
EMPTY_INPUT
Inheritance-based Template Method couples subclasses to the parent workflow and its protected members. If every small variation needs another hook or subclass, even a simple transformation becomes sensitive to parent changes; a function parameter or Strategy is a better fit. This alternative preserves the trim, validate, format order and explicit failure result.
Microsoft: Introduction to LINQ queries