Typorithm · medium

13. Classes, private state and composition

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

13. Classes, private state and composition

0%

Concept and contract

Distinguish static visibility, runtime privacy and replaceable capabilities.

A class creates both a runtime constructor and an instance type. Public instance members participate in structural compatibility, while private and protected members affect compatibility differently. Choose a class when state and lifecycle belong together, not simply because a language supports class syntax.

TypeScript private restricts access during checking, but JavaScript #private fields enforce runtime privacy. Neither is an authorization system. A private token still needs appropriate storage and access controls outside the object, and a public method may expose it if implemented carelessly.

Mechanism and worked example

The example stores a private count behind increment and value methods.

The example stores a private count behind increment and value methods. #count cannot be accessed with ordinary property syntax outside the class. The methods establish a small state transition and a read-only observation rather than exposing a mutable public counter.

Composition lets another object depend on a capability such as CounterLike instead of a particular inheritance tree. Keep the consumer’s interface minimal. When overriding inherited behavior, noImplicitOverride helps reveal renamed or accidentally shadowed members, but it does not prove behavioral substitutability.

Complete worked example

typescript

class Counter {
  #count = 0;
  increment(): void { this.#count++; }
  value(): number { return this.#count; }
}
const counter = new Counter();
counter.increment();
Reasoning
Only public operations expose and change the private count.

Counterexamples and common mistakes

The rejected private-field access demonstrates a runtime-backed privacy boundary in the language syntax.

The rejected private-field access demonstrates a runtime-backed privacy boundary in the language syntax. Replacing #count with a public count merely to satisfy a test changes the class contract. Test through public operations unless the internal representation is itself the subject of a carefully scoped diagnostic.

Avoid calling overridable methods from constructors when subclass state may not yet be initialized. Also avoid large base classes used only to share unrelated utilities. A composed dependency is often easier to fake and reason about than implicit inherited behavior.

Rejected counterexample: expected compiler diagnostic

typescript

class Counter { #count = 0; }
const counter = new Counter();
// @ts-expect-error Private identifiers cannot be accessed outside their class.
counter.#count;
Reasoning
The private identifier is invalid outside its declaring class, independent of interface shape.

Independent practice

Implement a bounded counter with a constructor-supplied nonnegative integer maximum.

Implement a bounded counter with a constructor-supplied nonnegative integer maximum. increment must stop at that maximum; value must report the current count. Reject invalid limits at runtime and keep internal state inaccessible through the public API.

Test maximum zero, repeated increments at the limit and an invalid fractional maximum. Add a small consumer depending only on a value method to show that reading state need not grant mutation capability. Do not use inheritance for this task.

Step 1: Validate the constructor maximum. Step 2: Bound each increment through private state.

Practice starter: solve before reading the solution

typescript

class BoundedCounter {
  constructor(maximum: number) { throw new Error("TODO"); }
  increment(): void {}
  value(): number { throw new Error("TODO"); }
}
Reasoning
Validate construction and enforce the maximum on every transition.
  • Hint 1: Zero is a valid maximum.
  • Hint 2: Observe the object through public methods.

Solution, tests and trade-offs

The solution validates the maximum once and keeps the mutable count in a #private field.

The solution validates the maximum once and keeps the mutable count in a #private field. increment enforces the transition invariant on every call. A readonly constructor parameter prevents reassignment through checked class code, while validation establishes the business restriction it represents.

Run the public-operation tests and a separate rejected private access. Explain why static readonly is not runtime freezing and why private state does not automatically make the whole application secure. The guarantee here is narrowly about this object’s accessible operations.

Practice solution and acceptance checks

typescript

class BoundedCounter {
  #count = 0;
  constructor(private readonly maximum: number) {
    if (!Number.isInteger(maximum) || maximum < 0) throw new Error("Invalid maximum");
  }
  increment(): void { if (this.#count < this.maximum) this.#count++; }
  value(): number { return this.#count; }
}

// Acceptance checks
const c=new BoundedCounter(1);c.increment();c.increment();if(c.value()!==1||new BoundedCounter(0).value()!==0)throw new Error("bound");let bad=false;try{new BoundedCounter(1.5)}catch{bad=true}if(!bad)throw new Error("validation");
Reasoning
A valid constructor input and a bounded transition jointly preserve the invariant.

Interview reasoning and next step

In interviews, compare interfaces, classes and factory functions for a stateful component.

In interviews, compare interfaces, classes and factory functions for a stateful component. Explain constructor value versus instance type, then describe the difference between private and #private. Mention lifecycle and substitutability rather than reciting access-modifier syntax.

Continue with declarations and JavaScript interoperability, where the runtime implementation may live outside the TypeScript source. That boundary requires even more care because declarations can promise behavior that the external code does not actually supply.

Optional stretch — after the core task works: Inject the read-only counter capability into a consumer.

Distinguish static visibility, runtime privacy and replaceable capabilities.