⚡IntermediateClasses & Decorators

Advanced Classes & Mixins

Go beyond basic classes with abstract classes, static blocks, private fields, and the mixin pattern for reusable, composable behaviors.

Overview

TypeScript classes have several advanced features that enable better architecture. **Abstract classes** define a template: they may contain concrete methods and abstract method signatures that subclasses must implement. They are the right tool when you have shared logic alongside required extension points. **Private class fields** (`#field`) are a JavaScript stage-4 feature that TypeScript supports - they are truly private at runtime (unlike the `private` keyword which is TypeScript-only and erased at compile time). This distinction matters when your code is inspected or called from plain JavaScript. **Mixins** are a pattern for composable, reusable behaviors without multiple inheritance. A mixin is a function that takes a base class and returns a new class extending it. You can apply multiple mixins to compose complex behavior from small, single-purpose pieces.

Key Points

  • •Abstract class: `abstract class Shape` - cannot be instantiated directly; provides partial implementation
  • •Abstract method: `abstract draw(): void` - no body; each subclass must implement it
  • •JavaScript private fields: `#field` - truly private at runtime; not visible even via reflection
  • •`private` keyword: TypeScript-only access control; erased to public at runtime; only TS catches violations
  • •Static blocks: `static { ... }` inside a class body run once when the class is initialized
  • •Mixin pattern: `type Constructor<T = {}> = new (...args: any[]) => T` - the reusable base for mixins
  • •Applying mixins: `class MyClass extends Timestamped(Activatable(Base)) {}`

Examples

Abstract classes

abstract class Shape {
  constructor(public color: string) {}

  // Concrete method  -  shared by all subclasses
  describe(): string {
    return `A ${this.color} ${this.constructor.name}`;
  }

  // Abstract  -  each subclass must implement this
  abstract area(): number;
  abstract perimeter(): number;
}

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }
  area(): number { return Math.PI * this.radius ** 2; }
  perimeter(): number { return 2 * Math.PI * this.radius; }
}

class Rectangle extends Shape {
  constructor(color: string, public width: number, public height: number) {
    super(color);
  }
  area(): number { return this.width * this.height; }
  perimeter(): number { return 2 * (this.width + this.height); }
}

// const s = new Shape('red'); // Error: cannot instantiate abstract class

const shapes: Shape[] = [new Circle('blue', 5), new Rectangle('red', 4, 6)];
for (const shape of shapes) {
  console.log(shape.describe(), '| area:', shape.area().toFixed(2));
}

Private fields vs private keyword

class Counter {
  // TypeScript 'private'  -  erased at runtime, accessible from JS
  private tsPrivate = 0;

  // JavaScript '#' private  -  truly private, enforced at runtime
  #jsPrivate = 0;

  increment(): void {
    this.tsPrivate++;
    this.#jsPrivate++;
  }

  getValues(): [number, number] {
    return [this.tsPrivate, this.#jsPrivate];
  }
}

const c = new Counter();
c.increment();
c.increment();
console.log(c.getValues()); // [2, 2]

// TypeScript catches both access violations at compile time:
// c.tsPrivate;   // Error in TS
// c.#jsPrivate;  // Error in TS

// But at RUNTIME in plain JavaScript:
// (c as any).tsPrivate  // works (it's just a property)
// c.#jsPrivate           // ReferenceError: truly private

Mixin pattern - composable behaviors

// The base constructor type for mixins
type Constructor<T = {}> = new (...args: any[]) => T;

// Timestamped mixin  -  adds createdAt and updatedAt
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();

    touch(): void {
      this.updatedAt = new Date();
    }
  };
}

// Activatable mixin  -  adds isActive toggle
function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActive = false;

    activate(): void   { this.isActive = true; }
    deactivate(): void { this.isActive = false; }
  };
}

// Base entity class
class Entity {
  constructor(public id: number) {}
}

// Compose mixins  -  order: innermost applied first
class User extends Timestamped(Activatable(Entity)) {
  constructor(id: number, public name: string) {
    super(id);
  }
}

const user = new User(1, 'Alice');
user.activate();
user.touch();

console.log(user.name);       // "Alice"
console.log(user.isActive);   // true
console.log(user.createdAt);  // Date
console.log(user.updatedAt);  // Date (after touch)

Common Mistakes

  • Assuming TypeScript's `private` keyword is truly private - it's erased to a regular property at runtime; use `#` fields for true encapsulation
  • Calling `super()` incorrectly in mixins - each mixin returns a class that extends the base, so `super(...args)` must forward all constructor arguments
  • Trying to instantiate an abstract class directly - `new AbstractClass()` is a compile error; always instantiate the concrete subclass

Note

Choose `abstract class` over a plain `interface` when you have shared implementation to provide alongside the extension contract. Choose `interface` when you only have the contract and no shared code. Mixins fill the gap between them - reusable behavior blocks without forcing a deep inheritance chain.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 3 exercises

Knowledge Check

Question 1 of 4

What is the difference between TypeScript's `private` keyword and JavaScript's `#` private fields?