094 — Interfaces with Classes (HasFormatter, implements, and the form handler)
Full companion narrative for TypeScript Tutorial #16 — the HasFormatter interface, implements on Invoice and Payment, HasFormatter-typed variables and arrays, and the dynamic form handler
Video: 094 · TypeScript Tutorial #16 - Interfaces with Classes · watch · ~11:08 · Net Ninja TypeScript tutorial series, lesson 16 (Shaun)
Source: this is a written companion to the video. Part of the Favorite TypeScript Videos curated path, module 05 (objects and interfaces). The payoff for 015: interfaces stop describing plain objects and start constraining classes.
the HasFormatter contract
“Right then my friends, so we’ve seen how to use interfaces to define the basic structure of objects in the last lesson. Now I want to show you how we can use interfaces alongside classes as well.”
A new folder (src/interfaces/) holds a new module (HasFormatter.ts) exporting a single-method interface. No properties — just the guarantee that anything implementing it can format itself as a string:
export interface HasFormatter {
format(): string;
}
“Now inside here, we can define the different properties or methods that anything which implements or uses this interface should have.”

Invoice implements the interface
The existing Invoice class adopts the contract with the implements keyword, importing the interface the same way 014 imported classes — with the .js extension in the specifier even though the source is .ts, because the browser loads the compiled output:
import { HasFormatter } from "../interfaces/HasFormatter.js";
export class Invoice implements HasFormatter {
constructor(
readonly client: string,
private details: string,
public amount: number,
) {}
format(): string {
return `${this.client} owes £${this.amount} for ${this.details}`;
}
}
“Now, if we want to implement the has formatter interface inside a class, then we can say, implements.”
The check is structural: Invoice already has a format(): string method, so it passes with no changes. Delete or mis-type format and the compiler errors — the class no longer satisfies the contract. That is the whole enforcement mechanism: implements turns a convention (“remember to add format”) into a compile-time rule.


a second class proves the contract is shareable
Payment is copied from Invoice into src/classes/Payment.ts with one semantic change — client becomes recipient, and the sentence flips direction:
export class Payment implements HasFormatter {
constructor(
readonly recipient: string,
private details: string,
public amount: number,
) {}
format(): string {
return `${this.recipient} is owed £${this.amount} for ${this.details}`;
}
}
Two classes, different shapes (client vs recipient), same format(): string guarantee.

the interface as a type
Once classes implement the interface, the interface name itself becomes a usable type — a variable typed HasFormatter accepts an instance of any class that implements it:
let docOne: HasFormatter;
let docTwo: HasFormatter;
docOne = new Invoice("yoshi", "web work", 250);
docTwo = new Payment("mario", "plumbing work", 200);
“So we’re saying here, in the future, this variable must be of type has formatter, it must implement that interface.”



The array version is the real win: docs accepts objects from unrelated classes, and every element is guaranteed to have format(): string — so a loop over docs can call .format() with no checks and no casts:
let docs: HasFormatter[] = [];
docs.push(docOne); // ok — Invoice implements HasFormatter
docs.push(docTwo); // ok — Payment implements HasFormatter

putting it to work in the form handler
The demo variables are deleted and the pattern moves into the real submit handler from 011. One HasFormatter-typed variable replaces the separate invoice/payment paths: read the <select> value, construct the matching class, store it in doc:
let doc: HasFormatter;
if (type.value === "invoice") {
doc = new Invoice(toFrom.value, details.value, amount.valueAsNumber);
} else {
doc = new Payment(toFrom.value, details.value, amount.valueAsNumber);
}
console.log(doc);
“Then under that, I’m gonna do an I check, so I’ll say if, and then the type, which is this thing right here, remember, the type is what they choose, right here.”


The submit handler demo shows both branches: submitting “payment to yoshi, marketing, £100” logs a Payment object; submitting “invoice to mario, website work, £200” logs an Invoice. Either branch satisfies doc’s type because both classes implement the contract — the handler no longer cares which class it built, only that the result can format itself. Next lesson preview: a class that takes these objects and renders them to the screen (the list template).

Three rules to carry forward: implements is a compile-time check only (no runtime code emitted, same as interfaces in 015); interface-typed positions accept any implementing instance regardless of class; and a shared method guarantee (format(): string) is what makes heterogeneous collections like HasFormatter[] safe to iterate.
Prev: 093 — Interfaces · Next: 095 — Rendering an HTML Template
The auto-transcript mishears the interface name (“HS formatter”, “HA format interface” — both mean HasFormatter); bare doc in narration refers to docOne/docTwo by context. Frame OCR drops the .js specifier once (HasFormatter.ts in an import) — the lesson’s own 014 rule applies: write .js.