Skip to content
academia.sh

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; private and protected are 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 abstract modifier does not remain in the compiled output.
  • implements is 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.

Start typing to search.

↑↓ Esc navigate · open · close