Files
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
Classes
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
| Modifier | Who can use it |
|---|---|
public (the default) | anyone |
protected | the class and its subclasses |
private | only 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.
MemoryStoreimplementsCatStore: it keeps cats in a private, readonly array,save()adds one, andall()answers a copy.CatsServicereceives aCatStorethrough its constructor and keeps it private and readonly. It has a static, readonlyMAX_AGEof 30 and a private id counter that starts at 1.create(name, age)stores and returns a new cat with the next id, and refuses an age below 0 or aboveMAX_AGEwith aRangeErrorsaying"age must be between 0 and 30", storing nothing.countis a getter: how many cats the store holds.olderThan(age)returns the cats strictly older thanage.
When it fails
Type 'MemoryStore' is missing the following properties from type 'CatStore': the class does not haveall()andsave()yet.Unused '@ts-expect-error' directivebesideservice.store: the store is public. Addprivate.- "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,protectedandreadonlyare compile-time promises; a getter reads like a field and cannot be set.staticmembers belong to the class;implementschecks 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().
Press Run tests to start the app. Its log appears here.Tests
- cats.service.spec.tsrun to see its tests