Emmanuel Akinfulubi

Lesson 10 of 1510 min read

Static Members

Some things belong to the class itself rather than to any one object built from it. The timetable on the wall is not a bus.

By the end: Put counters, factories and constants where they belong.

The timetable pinned to the park wall is not a bus. It describes buses, it is shared by all of them, and asking it "how many seats do you have?" is a category error.

A static member belongs to the class itself, not to any instance built from it. You reach it through the class name.

Static fields and methods

class Bus {
  static built = 0;
  static MAX_SEATS = 59;

  constructor(plate) {
    this.plate = plate;
    Bus.built++;
  }

  static census() {
    return `${Bus.built} buses registered so far`;
  }
}

new Bus('LAG-441-XA');
new Bus('ABJ-772-KM');
new Bus('ENU-318-PH');

console.log(Bus.census());
console.log('limit:', Bus.MAX_SEATS);

// Not on the instances. This is the whole point.
const b = new Bus('KAN-905-BD');
console.log('b.built:', b.built);
console.log('b.census:', b.census);
console.log(Bus.census());

Output


      

The four jobs statics actually do

1. Named constructors, which read far better than a flag argument

class Departure {
  constructor(route, minutes) {
    this.route = route;
    this.minutes = minutes;      // minutes past midnight
  }

  static fromClock(route, clock) {
    const [hours, mins] = clock.split(':').map(Number);
    return new Departure(route, hours * 60 + mins);
  }

  static fromJSON(json) {
    const data = JSON.parse(json);
    return new Departure(data.route, data.minutes);
  }

  static firstRun() {
    return new Departure('Lagos - Abuja', 5 * 60);
  }

  toString() {
    const hours = Math.floor(this.minutes / 60);
    const mins = String(this.minutes % 60).padStart(2, '0');
    return `${this.route} departs ${hours}:${mins}`;
  }
}

console.log(String(Departure.fromClock('Lagos - Abuja', '6:30')));
console.log(String(Departure.fromJSON('{"route":"Lagos - Kano","minutes":420}')));
console.log(String(Departure.firstRun()));

Output


      

2. Constants that belong to the concept

class Seat {
  static ROWS = Object.freeze(['A', 'B', 'C', 'D']);
  static PRICE = Object.freeze({ economy: 12000, luxury: 18000 });

  constructor(row, number, tier = 'economy') {
    if (!Seat.ROWS.includes(row)) {
      throw new RangeError(`Row ${row} does not exist. Rows are ${Seat.ROWS.join(', ')}.`);
    }
    this.row = row;
    this.number = number;
    this.tier = tier;
  }

  get price() { return Seat.PRICE[this.tier]; }
  get label() { return `${this.row}${this.number}`; }
}

const s = new Seat('D', 12, 'luxury');
console.log(s.label, 'costs', s.price);

try { new Seat('Z', 1); } catch (err) { console.log('refused:', err.message); }

// Frozen, so a caller cannot redefine your price list. In strict mode the
// attempt throws rather than being quietly dropped.
try {
  Seat.PRICE.economy = 1;
} catch (err) {
  console.log(err.name + ':', err.message);
}
console.log('still:', Seat.PRICE.economy);

Output


      

3. Utilities that need no instance

class Clock {
  static parse(text) {
    const match = /^(\d{1,2}):(\d{2})$/.exec(text);
    if (!match) throw new SyntaxError(`Not a time: "${text}"`);
    return Number(match[1]) * 60 + Number(match[2]);
  }

  static format(minutes) {
    const hours = Math.floor(minutes / 60);
    const mins = String(minutes % 60).padStart(2, '0');
    return `${hours}:${mins}`;
  }
}

console.log(Clock.parse('12:30'));
console.log(Clock.format(750));
try { Clock.parse('12-30'); } catch (err) { console.log(err.name + ':', err.message); }

Output


      

4. Registries and counters shared across every instance

class Trip {
  static #all = [];

  constructor(route) {
    this.route = route;
    Trip.#all.push(this);
  }

  static get count() { return Trip.#all.length; }

  static find(route) {
    return Trip.#all.find(t => t.route === route) ?? null;
  }

  static routes() {
    return Trip.#all.map(t => t.route);
  }
}

new Trip('Lagos - Abuja');
new Trip('Lagos - Kano');

console.log('count:', Trip.count);
console.log('routes:', Trip.routes());
console.log('found:', Trip.find('Lagos - Kano'));
console.log('missing:', Trip.find('Lagos - Jos'));

Output


      

this inside a static method

In a static method, this is the class. That is occasionally useful and occasionally the cause of a subtle bug with subclasses.

this is the class, which matters once you subclass

class Bus {
  static built = 0;

  static make(plate) {
    // `this` here is whichever class the call was made on.
    this.built++;
    return new this(plate);
  }

  constructor(plate) { this.plate = plate; }
  toString() { return `${this.constructor.name}(${this.plate})`; }
}

class LuxuryBus extends Bus {
  static built = 0;               // its own counter
}

const a = Bus.make('ABJ-772-KM');
const b = LuxuryBus.make('LAG-441-XA');

console.log(String(a), String(b));
console.log('Bus.built:', Bus.built);
console.log('LuxuryBus.built:', LuxuryBus.built);
console.log('b is a LuxuryBus:', b instanceof LuxuryBus, '| and a Bus:', b instanceof Bus);

Output


      

new this(plate) is the trick worth remembering. Because this is the class the call was made on, LuxuryBus.make() returns a LuxuryBus, not a Bus. Writing new Bus(plate) there would have quietly broken every subclass.

Statics are inherited by subclasses

The chain of classes carries statics

class Base {
  static tag = 'base';
  static describe() { return `I am ${this.tag}`; }
}

class Middle extends Base {}
class Leaf extends Middle {
  static tag = 'leaf';
}

console.log(Middle.describe());   // inherits both, so tag is still 'base'
console.log(Leaf.describe());     // inherits describe, overrides tag
console.log('Middle inherits from Base:', Object.getPrototypeOf(Middle) === Base);

Output


      

The shared-static trap

class Base {
  static log = [];
  static record(x) { this.log.push(x); }
}
class A extends Base {}
class B extends Base {}

A.record('from A');
B.record('from B');

console.log('A.log:', A.log);
console.log('B.log:', B.log);
console.log('same array?', A.log === B.log);

Output


      

Your turn: an id issuer

Write a Ticket class. Keep a private static counter. Every ticket gets a sequential id like T-001, assigned in the constructor, exposed read-only. Add a static issued getter and a static reset(). Then make three tickets and check the ids.

Editable. Fill it in, then run it.

Output


      

Recap

  • A static member belongs to the class, not to instances, and is reached through the class name.
  • Instances cannot see statics; from inside a method use ClassName.thing or this.constructor.thing.
  • Statics earn their keep as named constructors, concept-level constants, stateless utilities, and private registries.
  • Inside a static method this is the class, so new this(...) keeps subclasses working.
  • Subclasses inherit statics, and an object-valued static is shared until a subclass declares its own.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.