TypeScript from the ground up

Classes and interfaces

Typed state, methods that say what they take, `private` for what is nobody else’s business, and an interface a class can sign.

Part 7 of 13

Intermediate15 min readBack to Courses

A class is a shape with behaviour

A class describes an object that also carries its own functions. TypeScript adds types to both halves: the fields hold a declared type, and every method has a signature like any function. The two halves are checked together: a method may read a field the class declares, and the field’s type follows into the method body.

A field written count = 0 is inferred as a number, and this.count = "many" is refused. State stops being a free-for-all the moment it has a type.

State, methods, and what is private

interface Measurable {
  total(): number;
}

class Counter implements Measurable {
  private count = 0;
  constructor(readonly name: string) {}

  add(step: number): void {
    this.count = this.count + step;
  }

  total(): number {
    return this.count;
  }
}

The keywords that keep promises

private is a promise to the rest of the program: this field is mine, and touching it from outside is a compile-time error. readonly promises a value will not change after it is set — a good default for anything a constructor receives.

implements Measurable is a promise in the other direction: this class must have everything the interface describes. Rename the method and the compile fails at the class, not in whichever caller notices first.

Interfaces first, classes second

The interface is the useful half: func(total: Measurable) accepts the counter, and it would accept anything else that can answer total() — a plain object included. The class is one implementation of a described capability, not the description itself.

This is why the order in this course is interfaces, then classes. Design what a thing can do, then build something that can do it.

The constructor shortcut

A constructor that only assigns its arguments can say so in the parameter list: constructor(readonly name: string) {}. TypeScript writes the field, the assignment and the type in one place, and readonly keeps the promise that it will not change.

The long form is still worth knowing, because it is what the shortcut compiles to: a declared field, then this.name = name. Seeing the two side by side makes the shorthand read as shorthand rather than magic.

Private is a compile-time door

Marking a field private stops other code from writing counter.count. It does not encrypt anything: the field is still on the object at run time, and the browser console can still show it. The protection is that no code was allowed to depend on it.

That is enough for most design work. The point is not secrecy; it is that the class can change its internals without breaking callers, because callers were never given the key.

How a class is checked against an interface

implements is checked once, at the class declaration. The compiler takes the interface’s member list and looks for each one on the class, comparing signatures: a method total(): number satisfies total(): number, and a method that returns string does not. Fields count as well, so an interface may describe data a class holds rather than a method it offers.

The check is structural, not nominal. A class that never mentions Measurable but happens to have total(): number is accepted wherever a Measurable is wanted. implements is documentation and an early warning, not a secret handshake; deleting it would not change which values are accepted.

What goes wrong

The missing member is the first error. The headline, Class ’Counter’ incorrectly implements interface ’Measurable’, names the class; the detail names the member and its expected type. Read the detail, not the headline, because the headline only says that something in the contract is unfulfilled.

A field declared but not initialised is the second. Under strict mode, count: number; with no assignment in the constructor is an error — the compiler is asking what this.count is before anyone sets it. Initialise it at the declaration or in the constructor, and the question goes away.

Three class-shaped traps. A method passed as a callback loses its this unless it is bound or written as an arrow property. private is erased, so it is not a security boundary. And a class is not automatically the right tool: if there is no state to protect and no behaviour to share, a plain object and an interface say the same thing with less machinery.

Try these next

  • Rewrite a constructor as constructor(readonly name: string) and print the name.
  • Add a deposit method that refuses a negative amount without touching the balance.
  • Implement the same interface with a plain object and pass it to a function that takes the interface.

A typed counter

Keep the count private, type the step as a number, and declare a `Measurable` interface with `total(): number` that the class implements.

Live Preview
Console output will appear here...

3 left to do

  • The count is private to the class
  • The step says it is a number
  • The class signs a Measurable interface

The interface goes above the class and lists the method signature. The class signs it with implements Measurable — after which the compiler checks the method is really there.

A wallet with a private balance

Keep the balance private, take the owner through a readonly constructor parameter, and make `deposit` refuse a negative amount.

Live Preview
Console output will appear here...

3 left to do

  • The balance is private to the class
  • The owner is readonly
  • A negative deposit is refused

constructor(readonly owner: string) declares and assigns the field at once; private balance = 0 keeps it inside the class. Return early when the amount is below zero.

Reach for private at the moment a field is written, not later. A field that was never public cannot have a caller depending on it, and that is a door that is much cheaper to keep shut.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does marking a field private actually do?