Lesson 09 / 23
Classes and Access Modifiers
Typing class fields, the compile-time and runtime counterparts of visibility modifiers, parameter properties, abstract classes, and the implements declaration.
Contents
The contracts so far have defined only data and operation signatures; the implementation came from a separate function each time. Classes hold the two together.
The Objects and Functions in JavaScript course established class syntax’s counterpart in the prototype model. TypeScript adds three things to that syntax: types for fields and methods, visibility modifiers (access modifier), and abstract declarations. Some of these additions are erased, some generate code; that distinction is this lesson’s center.
Typing Fields and Visibility
A log class that collects measurement records shows all three visibility forms together:
interface Measurement { readonly id: string; value: number; } class MeasurementLog { private entries: Measurement[] = []; #hiddenCounter = 0; constructor(readonly name: string) {} add(measurement: Measurement): void { this.entries.push(measurement); this.#hiddenCounter += 1; } get count(): number { return this.entries.length; } } const log = new MeasurementLog("boiler-2"); log.add({ id: "s-01", value: 21.4 }); console.log(log.name, log.count);
Compiled with tsc --strict --target es2022 log.ts, the file named log.ts produces
this JavaScript:
"use strict"; class MeasurementLog { name; entries = []; #hiddenCounter = 0; constructor(name) { this.name = name; } add(measurement) { this.entries.push(measurement); this.#hiddenCounter += 1; } get count() { return this.entries.length; } } const log = new MeasurementLog("boiler-2"); log.add({ id: "s-01", value: 21.4 }); console.log(log.name, log.count);
node log.js outputs boiler-2 1. Three things stand out in the output.
private is erased. The private entries field remains entries in the compiled
code and is readable from outside. #hiddenCounter stays. This is a feature
belonging to JavaScript, not TypeScript, and it is enforced at runtime.
The constructor(readonly name: string) syntax generates code. This is called a
parameter property: when a visibility or readonly modifier is placed on a
constructor parameter, the compiler declares a field with the same name and adds the
assignment. Like enums, this is an exception to the type-erasure principle.
The consequence of this difference is observed when the compiled class is used directly from JavaScript. The file below takes the class body generated above and adds three queries:
class MeasurementLog { name; entries = []; #hiddenCounter = 0; constructor(name) { this.name = name; } add(measurement) { this.entries.push(measurement); this.#hiddenCounter += 1; } get count() { return this.entries.length; } } const log = new MeasurementLog("boiler-2"); log.add({ id: "s-01", value: 21.4 }); console.log(Object.keys(log)); console.log(log.entries); console.log(log.hiddenCounter);
[ 'name', 'entries' ]
[ { id: 's-01', value: 21.4 } ]
undefined
The entries field is both enumerable and readable; #hiddenCounter, on the other
hand, gives undefined when accessed under the name hiddenCounter, because no such
public field exists.
At compile time, both are protected, but with different diagnostics:
class MeasurementLog { private records: number[] = []; #counter = 0; add(value: number): void { this.records.push(value); this.#counter += 1; } } const log = new MeasurementLog(); log.add(21.4); console.log(log.records); console.log(log.#counter);
i1.ts(13,17): error TS2341: Property 'records' is private and only accessible within class 'MeasurementLog'. i1.ts(14,17): error TS18013: Property '#counter' is not accessible outside class 'MeasurementLog' because it has a private identifier.
The selection criterion: private if visibility is a design rule, # if it is a
security boundary. Within a team, private is enough to prevent misuse, and it
catches it at compile time. At a library boundary, where unaudited code must not have
access, # is used.
The protected modifier is likewise valid only at compile time, and it opens the
field to the class itself and its subclasses.
Abstract Classes
An abstract class is a class that can declare unimplemented members and cannot be instantiated directly. If the measurement source contract is also going to carry shared behavior, it is written with an abstract class instead of an interface:
interface Measurement { id: string; value: number; } abstract class MeasurementSource { abstract readonly name: string; protected abstract next(): Measurement | null; all(): Measurement[] { const collected: Measurement[] = []; let record = this.next(); while (record !== null) { collected.push(record); record = this.next(); } return collected; } } class ArraySource extends MeasurementSource { readonly name = "array"; private index = 0; constructor(private readonly records: readonly Measurement[]) { super(); } protected next(): Measurement | null { return this.index < this.records.length ? this.records[this.index++] : null; } } const source = new ArraySource([ { id: "s-01", value: 21.4 }, { id: "s-02", value: 22.1 }, ]); console.log(source.name, source.all().length);
Output is array 2.
The structure separates two kinds of members. The next method is abstract: every
source implements it differently. The all method is concrete and carries the shared
behavior built on top of next. The subclass only writes the part that changes.
If the line const impossible = new MeasurementSource(); is added to the end of the
file, after a blank line, the compiler gives this diagnostic:
i2.ts(40,20): error TS2511: Cannot create an instance of an abstract class.
The abstract modifier is erased too — an ordinary class remains in the compiled
output, and it can be instantiated by JavaScript. The prohibition belongs to compile
time.
implements
That a class satisfies a specific contract is declared with implements:
interface MeasurementSource { readonly name: string; next(): number | null; } class EmptySource implements MeasurementSource { readonly name = "empty"; }
i4.ts(6,7): error TS2420: Class 'EmptySource' incorrectly implements interface 'MeasurementSource'. Property 'next' is missing in type 'EmptySource' but required in type 'MeasurementSource'.
implements is a check declaration, not an inheritance. It adds no member to the
class; it only verifies conformance to the contract and gives the diagnostic on the
class’s declaration line. Without this declaration the class would still compile, and
the mismatch would only surface at the point of use.
The implements syntax is erased entirely; no trace of it remains in the compiled
output.
Abstract Class or Interface
The two tools overlap; the criterion is this:
| Criterion | Interface | Abstract class |
|---|---|---|
| Carries a shared implementation | No | Yes |
| Generates runtime code | No | Yes |
| A type satisfies more than one | Yes | No (single superclass) |
| Carries state (field values) | No | Yes |
An interface when only a contract is needed. It is erased, more than one can be used together, and it does not bind the implementation.
An abstract class when shared behavior is needed along with the contract. The
all method above is an example: an algorithm written in one place and valid across
every source.
The class invariant concept introduced in the Programming Fundamentals course
finds type-level support here: an invariant held over private fields cannot be
broken from outside — because only the class’s own methods can access those fields,
and the compiler enforces it.
Summary
- Class field and method types are erased;
privateandprotectedare valid only at compile time, while fields declared with#are protected at runtime too. - A parameter property (
constructor(readonly name: string)) generates code: the compiler adds the field declaration and the assignment. - An abstract class declares unimplemented members and cannot be instantiated
directly; the
abstractmodifier does not remain in the compiled output. implementsis a check declaration; it adds no member to the class, gives the diagnostic on the declaration line, and is erased entirely.- An interface is chosen for a contract alone; an abstract class is chosen for a contract together with shared behavior.
Next Step
Class methods and standalone functions were only used in this lesson; how their types
are written was not covered. A function’s type is not just parameter and return
types: optional parameters, rest parameters, overloading, and the this context are
also part of the type. The next lesson builds function types in detail.
To keep your progress and take notes, Log in
My notes
Log in to take notes.