BehavioralTypeScriptverifiedVerified
Template Method Pattern in TypeScript
Defines the skeleton of an algorithm in a base class, deferring certain steps to subclasses without changing the algorithm's overall structure.
How to Implement the Template Method Pattern in TypeScript
1Step 1: Define the abstract template with hook methods
abstract class DataMiner {
// Template method — defines the algorithm skeleton
mine(source: string): string[] {
const raw = this.extractData(source);
const parsed = this.parseData(raw);
const filtered = this.filterData(parsed);
this.reportResults(filtered);
return filtered;
}
protected abstract extractData(source: string): string;
protected abstract parseData(raw: string): string[];
// Hook — subclasses may override
protected filterData(data: string[]): string[] {
return data;
}
// Default implementation
protected reportResults(data: string[]): void {
console.log(`Mined ${data.length} records.`);
}
}2Step 2: Implement concrete miners that override steps
class CsvMiner extends DataMiner {
protected extractData(source: string): string {
return `CSV content of ${source}`;
}
protected parseData(raw: string): string[] {
return raw.split("\n").filter(Boolean);
}
}
class JsonMiner extends DataMiner {
protected extractData(source: string): string {
return `{"data": ["a","b","c"]}`;
}
protected parseData(raw: string): string[] {
return JSON.parse(raw).data as string[];
}
protected filterData(data: string[]): string[] {
return data.filter(item => item !== "b");
}
}3Step 3: Run the algorithm with different data sources
new CsvMiner().mine("report.csv");
new JsonMiner().mine("api/v1/data");// ── ETL Pipeline with Template Method ─────────────────────────────
interface PipelineMetrics {
source: string;
extractedCount: number;
transformedCount: number;
loadedCount: number;
errorCount: number;
durationMs: number;
}
interface PipelineRecord {
id: string;
[key: string]: unknown;
}
abstract class ETLPipeline<TRaw, TTransformed extends PipelineRecord> {
private metrics: Partial<PipelineMetrics> = {};
// Template method — orchestrates the full ETL flow
async run(source: string): Promise<PipelineMetrics> {
const start = Date.now();
this.metrics = { source, errorCount: 0 };
await this.beforeExtract(source);
const rawRecords = await this.extract(source);
this.metrics.extractedCount = rawRecords.length;
const transformed: TTransformed[] = [];
for (const record of rawRecords) {
try {
const result = await this.transform(record);
if (result) transformed.push(result);
} catch (err) {
this.metrics.errorCount = (this.metrics.errorCount ?? 0) + 1;
this.onTransformError(record, err as Error);
}
}
this.metrics.transformedCount = transformed.length;
const loaded = await this.load(transformed);
this.metrics.loadedCount = loaded;
await this.afterLoad(transformed);
return { ...this.metrics, durationMs: Date.now() - start } as PipelineMetrics;
}
// Abstract steps — subclasses must implement
protected abstract extract(source: string): Promise<TRaw[]>;
protected abstract transform(record: TRaw): Promise<TTransformed | null>;
protected abstract load(records: TTransformed[]): Promise<number>;
// Hooks — subclasses may override
protected async beforeExtract(_source: string): Promise<void> {}
protected async afterLoad(_records: TTransformed[]): Promise<void> {}
protected onTransformError(record: TRaw, error: Error): void {
console.error("Transform error:", error.message, record);
}
}
// ── Concrete Pipeline ─────────────────────────────────────────────
interface RawUser { name: string; email: string; created_at: string }
interface User extends PipelineRecord { id: string; name: string; email: string; createdAt: Date }
class UserImportPipeline extends ETLPipeline<RawUser, User> {
private db: User[] = [];
protected async extract(_source: string): Promise<RawUser[]> {
// Simulate CSV/API fetch
return [
{ name: "Alice", email: "[email protected]", created_at: "2024-01-15" },
{ name: "Bob", email: "[email protected]", created_at: "2024-02-20" },
{ name: "", email: "invalid", created_at: "bad-date" },
];
}
protected async transform(record: RawUser): Promise<User | null> {
if (!record.name || !record.email.includes("@")) return null;
const date = new Date(record.created_at);
if (isNaN(date.getTime())) throw new Error(`Invalid date: ${record.created_at}`);
return { id: crypto.randomUUID(), name: record.name, email: record.email, createdAt: date };
}
protected async load(records: User[]): Promise<number> {
this.db.push(...records);
return records.length;
}
protected async afterLoad(records: User[]): Promise<void> {
console.log(`Loaded ${records.length} users. Total in DB: ${this.db.length}`);
}
getDb(): User[] { return this.db; }
}
export { ETLPipeline, UserImportPipeline, type PipelineMetrics, type PipelineRecord };Template Method Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Template Method Pattern in the Real World
“Consider a recipe for baking bread. The overall process—mix, knead, let rise, bake, cool—is fixed. But the specific flour blend, kneading technique, and baking temperature are decisions left to the baker. The cookbook provides the invariant sequence; individual bakers customize the steps that can vary without disrupting the overall process.”