InteractiveFrameworks

Classes

A service that receives its store through its constructor and hides what it keeps, the shape of every provider in NestJS: parameter properties, private and readonly, static members, getters, and a class that implements an interface.

What you'll learn

  • Declare fields, a constructor and methods, and use parameter properties to receive and keep a dependency
  • Hide state with private and protect it with readonly, and share a constant with static
  • Implement an interface, and depend on the interface so any object with its shape can be passed in

Every provider in NestJS is a class: a service that keeps some state, offers methods, and receives what it depends on through its constructor. constructor(private readonly catsService: CatsService) {} is the most common line in a Nest app, and it packs four ideas into one. This lesson unpacks them: fields and methods, the parameter property that line uses, what private, readonly and static promise, and why a class should depend on an interface rather than on another class.

Fields, constructor, methods

class Counter {
  count = 0;                       // a field, with its initial value
  constructor(readonly label: string) {}
  increment(): number {
    this.count += 1;               // methods reach the instance through this
    return this.count;
  }
}

new Counter('visits') builds an instance: fields are initialised, the constructor runs, and every instance has its own count.

Parameter properties

The constructor above has a parameter property: a modifier (readonly, private, public or protected) in front of a constructor parameter declares a field of that name and assigns the argument to it. These two classes are the same:

class Mailer {
  private readonly transport: Transport;
  constructor(transport: Transport) {
    this.transport = transport;
  }
}

class Mailer {
  constructor(private readonly transport: Transport) {}
}

That is the Nest line: receive a dependency, keep it, hide it, and never replace it.

Access modifiers and readonly

ModifierWho can use it
public (the default)anyone
protectedthe class and its subclasses
privateonly the class's own code

readonly forbids assignment after construction. Like every type-level rule, both are checked by the compiler and gone at runtime; JavaScript's own #field syntax is private at runtime too, and TypeScript supports it, but Nest code mostly uses private.

A getter exposes a computed value that reads like a field, service.count rather than service.count(), and without a setter it cannot be assigned. A static member belongs to the class itself, not to each instance: Invoice.TAX_RATE, shared by all of them.

class Invoice {
  static readonly TAX_RATE = 0.21;
  constructor(private readonly lines: number[]) {}
  get total(): number {
    return this.lines.reduce((sum, amount) => sum + amount, 0) * (1 + Invoice.TAX_RATE);
  }
}

implements, and depending on shapes

class MemoryQueue implements Queue asks the compiler to check that the class has everything the Queue interface requires. It changes nothing at runtime; it is a promise, checked.

The bigger idea is on the other side. A class that receives its dependency typed as an interface, constructor(private readonly queue: Queue), works with any object of that shape: the real queue in production, an in-memory one in development, a fake in a test that records what it was given. Because TypeScript compares shapes, the fake does not even need to be a class. This is what makes Nest code testable, and the given spec does exactly that: it builds the service on a plain object with all() and save().

Hiding state properly

private stops other code from reaching a field by name, and then a method hands the field straight out: all() { return this.items; } gives the caller the very array, and push on it changes the store. A class that owns a collection answers a copy, [...this.items], so its state changes only through its own methods.

Your task

The work is in cats.service.ts; the spec's compiler checks list what the classes must keep to themselves.

  1. MemoryStore implements CatStore: it keeps cats in a private, readonly array, save() adds one, and all() answers a copy.
  2. CatsService receives a CatStore through its constructor and keeps it private and readonly. It has a static, readonly MAX_AGE of 30 and a private id counter that starts at 1.
  3. create(name, age) stores and returns a new cat with the next id, and refuses an age below 0 or above MAX_AGE with a RangeError saying "age must be between 0 and 30", storing nothing.
  4. count is a getter: how many cats the store holds.
  5. olderThan(age) returns the cats strictly older than age.

When it fails

  • Type 'MemoryStore' is missing the following properties from type 'CatStore': the class does not have all() and save() yet.
  • Unused '@ts-expect-error' directive beside service.store: the store is public. Add private.
  • "refuses an age outside 0 to MAX_AGE" saves a cat: the check runs after save().
  • "answers a copy" finds two cats: all() returns the array itself.

Remember

  • A parameter property receives and keeps a dependency in one line.
  • private, protected and readonly are compile-time promises; a getter reads like a field and cannot be set.
  • static members belong to the class; implements checks a class against an interface.
  • Depend on an interface, and any object of that shape, a fake included, can be passed in.
Stuck? Show a hint

constructor(private readonly store: CatStore) {} declares, receives and keeps the store in one line. static readonly MAX_AGE = 30 belongs to the class: read it as CatsService.MAX_AGE. A getter is get count(): number { … }, read without parentheses. class MemoryStore implements CatStore makes the compiler check it has all() and save().