Classes and objects

You already know how to bundle related facts together as a record: one pupil with a name, an age and an isPresent flag, all in a single value. That's a big step — but a record only holds data. It just sits there. Real things don't only have facts about them; they can do things too.

Think about a bank account. It has data — an owner's name and a balance — but it also has behaviour: you can pay money in, take money out, and ask how much is left. The data and the actions belong together: it makes no sense to "pay in" without an account to pay into. What we'd love is a way to package the data and the behaviour that acts on that data into one tidy unit. That unit is a class, and this is the heart of object-oriented programming (OOP).

The blueprint is written once; from it you can stamp out as many objects as you like, each with its own values. Let's build one.

The blueprint: a class with fields, a constructor, and methods

Here is a whole BankAccount class. Read it slowly — every part has a name, and we'll take them one at a time underneath.

class BankAccount { owner: string; // a field — data every account carries balance: number; // another field // the constructor: runs once, when a new account is made constructor(owner: string, opening: number) { this.owner = owner; // set THIS account's owner this.balance = opening; // set THIS account's starting balance } // a method — behaviour that acts on this account's data deposit(amount: number): void { this.balance = this.balance + amount; } withdraw(amount: number): void { this.balance = this.balance - amount; } report(): string { return this.owner + " has £" + this.balance; } } // make ONE object (instance) from the blueprint: const acc = new BankAccount("Aisha", 100); acc.deposit(50); acc.withdraw(30); console.log(acc.report()); // Aisha has £120

Four ideas are doing all the work here:

Making an object: new

A class on its own does nothing — it's only a design, like the plans for a house. To get something you can actually use, you build an object from it with the keyword new:

const acc = new BankAccount("Aisha", 100); // └──┘ └────────┘ └──────────┘ // new the class arguments passed to the constructor

Reading it left to right: new asks for a fresh object, BankAccount(...) runs the constructor with the arguments "Aisha" and 100, and the finished object is handed back and stored in acc. From then on you reach an object's fields and call its methods with a dot — exactly like a record's fields — acc.balance, acc.deposit(50).

The picture above is the whole idea in one image: one blueprint on the left, and the three separate objects it stamped out on the right. Each object has the same shape (an owner and a balance) but its own values.

Many objects, one class — each keeps its own state

The real power shows up the moment you make more than one object. Every call to new produces a brand-new, independent account. Paying into one leaves the others completely untouched, because each has its own balance field. Run this:

class BankAccount { owner: string; balance: number; constructor(owner: string, opening: number) { this.owner = owner; this.balance = opening; } deposit(amount: number): void { this.balance = this.balance + amount; } report(): string { return this.owner + ": £" + this.balance; } } const a = new BankAccount("Aisha", 100); const b = new BankAccount("Ben", 0); a.deposit(50); // only Aisha's account changes console.log(a.report()); // Aisha: £150 console.log(b.report()); // Ben: £0 — untouched!

a and b are two different objects made from the same class. When we call a.deposit(50), inside deposit the word this means a, so only a.balance grows. Ben's account never hears about it. That separateness is what makes objects so useful: you can model a whole bank — thousands of accounts — from a single blueprint, and each keeps its own state.

A second blueprint, so the pattern sticks: Dog

The same recipe works for anything, not just money. Here a Dog class carries a name and an age, and knows how to bark and to have a birthday. Notice how a method can change a field (birthday nudges age up) and how it can read other fields (describe uses both):

class Dog { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } bark(): string { return this.name + " says Woof!"; } birthday(): void { this.age = this.age + 1; // this dog gets one year older } describe(): string { return this.name + " is " + this.age + " years old."; } } const rex = new Dog("Rex", 3); const bella = new Dog("Bella", 7); console.log(rex.bark()); console.log(bella.describe()); rex.birthday(); // only Rex ages console.log(rex.describe()); // Rex is 4 years old. console.log(bella.describe()); // Bella is 7 years old. — unchanged

Same four ingredients, completely different subject. Once you can spot fields, constructor, methods and this, you can read almost any class you meet.

Inside a method, this always means "the object to the left of the dot on the call that got me here". Write rex.bark() and, while bark runs, this is rex; write bella.bark() and the very same code now has this pointing at bella. One set of instructions, reused by every object, each time bound to whichever object it was called on. That's why we don't have to write bark separately for every dog — the method is shared, but this makes it act on the right animal.

Classes build on records — but do more

You met a TypeScript interface when learning about records: it describes the shape of some data so the computer can check it. A class does that too — every object has typed fields — but it goes further by attaching behaviour and a constructor to that shape. Compare the two side by side:

// A record + interface: shape only. You build it by hand and write free functions to act on it. interface AccountData { owner: string; balance: number; } const acc: AccountData = { owner: "Aisha", balance: 100 }; function deposit(a: AccountData, amount: number) { a.balance += amount; } deposit(acc, 50); // A class: shape AND behaviour AND setup, packaged together. class BankAccount { owner: string; balance: number; constructor(owner: string, balance: number) { this.owner = owner; this.balance = balance; } deposit(amount: number) { this.balance += amount; } } const acc2 = new BankAccount("Aisha", 100); acc2.deposit(50); // the behaviour travels WITH the object

With the record you must remember to call the right free function on the right data. With the class, the deposit behaviour lives with the account, so you simply ask the object to do it: acc2.deposit(50). Bundling data with the behaviour that belongs to it is the idea OOP is built on.

The single most common muddle for beginners is confusing the class with an object. Hold this picture in your head:

Two things follow from this. First, you don't store data in the class — you store it in the objects; BankAccount.balance is meaningless, acc.balance is real. Second, every new makes a completely fresh, independent object:

const a = new BankAccount("Aisha", 100); const b = new BankAccount("Aisha", 100); // a and b happen to hold the same values, but they are DIFFERENT objects. a.deposit(50); // now a has £150 and b still has £100 — changing one never touches the other.

So a class is a noun-you-design; an object is a noun-that-exists. "Dog" is a class; Rex and Bella are objects. Mixing them up leads to expecting one cookie to change when you reshape the cutter — it won't.