---
title: 'Classes and Access Modifiers'
source: 'https://academia.sh/en/courses/typescript/classes-and-access-modifiers'
course: TypeScript
language: en
updated: '2026-08-17T18:09:53+00:00'
license: 'CC BY-SA 4.0'
---

# Classes and Access Modifiers

Typing class fields, the compile-time and runtime counterparts of visibility modifiers, parameter properties, abstract classes, and the implements declaration.

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:

```typescript
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:

```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:

```javascript
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);
```

```text
[ '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:

```typescript
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);
```

```text
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:

```typescript
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:

```text
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`:

```typescript
interface MeasurementSource {
  readonly name: string;
  next(): number | null;
}

class EmptySource implements MeasurementSource {
  readonly name = "empty";
}
```

```text
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.
