TypeScript Survival Guide

Favorite typescript videos

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.”

new interfaces folder and HasFormatter.ts

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.

Invoice with implements HasFormatter

implements clause on the class declaration

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.

Payment.ts created alongside Invoice

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.”

three imports and HasFormatter-typed variables in app.ts

docOne and docTwo assigned from different classes

demo variables beside the instance code

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

docs array logged with both objects

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.”

submit handler with the if/else construction

branching on the select value

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).

Finance Logger page with submitted entries

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.