Lesson 5 • Intermediate
Classes and Objects 🏛️
By the end of this lesson you'll design your own classes in TypeScript — with typed fields, constructors, access modifiers, getters/setters, interfaces, and inheritance — the toolkit behind every real-world TypeScript app.
What You'll Learn
- Write a class with typed fields and a constructor
- Use parameter properties to declare and assign in one line
- Control access with public, private, protected, and readonly
- Add getters and setters that run code behind a property
- Make a class implement an interface (a contract)
- Extend a parent class with extends and super, plus generics in classes
private, readonly) are compile-time checks, so the runnable demos below use plain JS classes, while the TypeScript-only syntax is shown in read-only blocks. Install TypeScript to see the type errors for real.new User(...)) is a separate object with that shape but its own values. Access modifiers are the building's security: public = the lobby (anyone), protected = staff floors (the class and its subclasses), private = the vault (this class only), readonly = the engraved address (set once, never changed).1. Classes, Fields & the Constructor
A class is a blueprint for objects. The constructor is a special method that runs once when you write new ClassName(...); its job is to store the starting values on this — the brand-new object being built. A method is just a function that lives on the object. Read this worked example, run it, then you'll write your own.
Worked example: a User class
See how the constructor builds each object and methods read this.
// A class is a BLUEPRINT. You stamp out objects from it with "new".
console.log("=== Building objects from a class ===");
class User {
// The constructor runs once, when you write "new User(...)".
// Its job: store the incoming values on "this" (the new object).
constructor(name, email, age) {
this.name = name; // this = the object being built
this.email = email;
this.age = age;
}
// A method = a function that lives on the object.
greet() { return "Hi, I'm " + this
...In TypeScript you add a type to every field and parameter, so the compiler catches mistakes before the code runs. You declare the fields above the constructor with name: string. TypeScript also offers a shortcut called parameter properties: put an access modifier on a constructor parameter and TypeScript declares the field and assigns it for you — no this.name = name boilerplate. (This block is read-only; it's TypeScript-specific syntax.)
// TypeScript version — note the type annotations the JS above hides.
class User {
// Declare each field WITH its type, above the constructor:
name: string;
email: string;
age: number;
constructor(name: string, email: string, age: number) {
this.name = name; // TS checks the types line up
this.email = email;
this.age = age;
}
greet(): string { // the ": string" is the RETURN type
return `Hi, I'm ${this.name} (${this.age})`;
}
}
// ---- Parameter properties: the same class, written shorthand ----
// A modifier (public/private/readonly) on a constructor PARAMETER
// declares the field AND assigns it for you. No "this.x = x" needed.
class UserShort {
constructor(
public name: string,
public email: string,
public age: number
) {} // empty body — TS wired it all up
greet(): string {
return `Hi, I'm ${this.name} (${this.age})`;
}
}2. Access Modifiers & readonly
Modifiers decide who can touch a field. public (the default) is readable everywhere; private is reachable only inside the same class; protected adds subclasses to that circle; and readonly lets a field be set once (in the constructor) and then locks it. These checks are TypeScript-only — they protect you while you type, then vanish at runtime — so this is a read-only block.
// Access modifiers control WHO can touch a field. (TypeScript-only.)
class BankAccount {
public owner: string; // ✅ readable everywhere (public is the default)
private balance: number; // 🔒 only inside THIS class
protected pin: string; // 🔒 this class AND subclasses, not outside
readonly id: string; // set once (in the constructor), then frozen
constructor(owner: string, balance: number, id: string) {
this.owner = owner;
this.balance = balance;
this.id = id; // ✅ readonly fields may be set in the constructor
}
deposit(amount: number): void {
this.balance += amount; // ✅ private is reachable from inside the class
}
}
const acc = new BankAccount("Alice", 1000, "ACC-001");
console.log(acc.owner); // ✅ "Alice" (public)
// console.log(acc.balance); // ❌ TS2341: 'balance' is private
// acc.id = "ACC-002"; // ❌ TS2540: 'id' is read-only3. Getters & Setters
A getter looks like a plain property when you read it (temp.fahrenheit, no parentheses) but actually runs code — great for values you want to compute on the fly. A setter runs when you assign (temp.celsius = 30), which is the perfect place to validate. By convention the backing field is named with a leading underscore, like _celsius.
Worked example: computed properties
Read a getter with no parentheses; a setter validates on assignment.
// Getters and setters look like a plain property but run CODE.
console.log("=== Getters & setters ===");
class Temperature {
constructor(celsius) { this._celsius = celsius; } // _name = "internal"
// "get" runs when you READ the property (no parentheses!)
get celsius() { return this._celsius; }
get fahrenheit() { return this._celsius * 9 / 5 + 32; }
// "set" runs when you ASSIGN to the property — perfect for validation
set celsius(value) {
if (value < -273.15) throw new Error
...Your turn. The Book class below is almost finished — fill in the five blanks marked ___ using the // 👉 hints, then run it and check your output.
🎯 Your turn: store fields in a constructor
Fill in the ___ blanks, then check against the expected output.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
console.log("=== Build a Book ===");
class Book {
constructor(title, author, pages) {
// 1) Store each parameter on "this":
this.title = ___; // 👉 title
this.author = ___; // 👉 author
this.pages = ___; // 👉 pages
}
// 2) Return a one-line summary using the fields:
describe() {
return ___ + " by " + ___ + " (" + this.pages + " pages)";
// 👉 use this.title and this.author
}
}
let
...4. Inheritance: extends & super
extends says "this class is a kind of that one" and inherits its fields and methods. Inside a subclass constructor you must call super(...) first — that runs the parent's constructor before you touch this. A subclass can override a method by redefining it; each object then uses its own version. Read the worked example, then complete the exercise below it.
Worked example: shapes inherit from Shape
super() runs the parent constructor; each subclass overrides area().
// extends = "is a kind of". super = "the parent's version".
console.log("=== Inheritance ===");
class Shape {
constructor(name, color) { this.name = name; this.color = color; }
describe() { return this.color + " " + this.name; }
area() { return 0; } // a default, overridden below
}
class Circle extends Shape {
constructor(radius, color) {
super("circle", color); // run Shape's constructor FIRST
this.radius = radius; // ...then add Circl
...Now you try. Make Dog inherit from Animal, call super in its constructor, and override speak(). Fill in the four blanks:
🎯 Your turn: extend a class
Wire up extends, super, and an overridden method.
// 🎯 YOUR TURN — finish the subclass using extends and super.
console.log("=== Animals ===");
class Animal {
constructor(name) { this.name = name; }
speak() { return this.name + " makes a sound"; }
}
// 1) Make Dog inherit from Animal:
class Dog extends ___ { // 👉 Animal
constructor(name, breed) {
// 2) Call the PARENT constructor first (it sets this.name):
___(name); // 👉 super
this.breed = breed;
}
// 3) Override speak() to return name + " bar
...5. Implementing an Interface
An interface is a contract: a list of methods and properties a class promises to provide. Writing class Product implements Comparable tells TypeScript "check that Product has everything Comparable requires." If a method is missing you get error TS2420 at compile time, long before users hit it. A class can implement several interfaces at once. Here's the TypeScript contract, then a runnable class that fulfils it:
interface Serializable {
toJSON(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
// One class, two contracts — TS checks BOTH are satisfied:
class Product implements Serializable, Comparable<Product> {
constructor(public name: string, public price: number) {}
toJSON(): string { return JSON.stringify({ name: this.name, price: this.price }); }
compareTo(other: Product): number { return this.price - other.price; }
}Worked example: a class that fulfils a contract
Because every Product has compareTo, the array sorts cleanly.
// In TS, "class Product implements Comparable" is a PROMISE that the
// class supplies every method the interface lists. JS has no interfaces,
// so here we just show the class that the contract describes.
console.log("=== implements a contract ===");
// TS interface (for reference):
// interface Comparable {
// compareTo(other: Product): number;
// }
class Product {
constructor(name, price) { this.name = name; this.price = price; }
// The method the interface required:
compare
...🔎 Deep Dive: Generics in Classes
Just like functions, a class can take a type parameter written in angle brackets, conventionally <T>. That lets one class work with any type while staying fully type-safe — a Box<number> only ever holds numbers, a Box<string> only strings, with no casting and no any.
class Box<T> {
private value: T;
constructor(value: T) { this.value = value; }
get(): T { return this.value; } // returns the SAME type you put in
}
const n = new Box<number>(42);
const s = new Box<string>("hi");
n.get(); // typed as number
s.get(); // typed as string
// new Box<number>("oops"); // ❌ TS2345: string is not assignable to numberYou'll meet this constantly in real code: Array<T>, Map<K, V>, and Promise<T> are all generic classes from the standard library.
Common Errors (and the fix)
- "TS2341: Property 'balance' is private and only accessible within class 'BankAccount'" — you read a
privatefield from outside. Expose it through apublicmethod or a getter (e.g.get balance()) instead of reaching in directly. - "TS2420: Class 'Product' incorrectly implements interface 'Comparable'" — the class is missing a method (or its signature is wrong) that the interface requires. Add the missing member with the exact name and types the interface lists.
- "TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor" (using a property before assignment) — every declared field must get a value in the constructor. Assign
this.name = ..., give it a default, or mark it optional withname?: string. - "'super' must be called before accessing 'this'" — in a subclass constructor, call
super(...)before any use ofthis. - "TS2540: Cannot assign to 'id' because it is a read-only property" — a
readonlyfield can only be set in the constructor. Remove the later assignment, or dropreadonlyif the value really must change.
Pro Tips
- 💡 Reach for parameter properties (
constructor(public name: string)) to delete repetitivethis.x = xlines. - 💡 Make fields
privateby default and open them up only when needed — it keeps your objects' internals safe to change later. - 💡 Need true runtime privacy? TypeScript's
privateis compile-time only; use a native#fieldfor privacy JavaScript enforces at runtime too. - 💡 Prefer interfaces + composition over deep inheritance. Long
extendschains get rigid fast; small contracts stay flexible and testable.
📋 Quick Reference
| Feature | Syntax | Meaning |
|---|---|---|
| Typed field | name: string; | A field that must hold text |
| Param property | constructor(public x: number) | Declares & assigns in one line |
| Private | private balance: number | Only inside this class |
| Protected | protected pin: string | This class + subclasses |
| Readonly | readonly id: string | Set once, then locked |
| Getter | get area() { ... } | Runs code on property read |
| Implements | class X implements Y, Z | Promise to fulfil a contract |
| Extends | class Dog extends Animal | Inherit from a parent |
| Generic class | class Box<T> { ... } | Works with any type, safely |
Frequently Asked Questions
Q: What's the difference between a class and an interface?
A class is real code you can new to build objects; it has fields and method bodies. An interface is only a type-checking contract — a list of what must exist, with no implementation. A class can implements an interface to prove it satisfies that contract.
Q: When should I use parameter properties vs declaring fields normally?
Use parameter properties when the constructor just stores its arguments — it removes the repetitive this.x = x lines. Declare fields the long way when a field isn't a direct constructor argument, or when it needs extra setup.
Q: Is TypeScript's private truly hidden at runtime?
No. private is enforced only by the compiler; the compiled JavaScript still exposes the field. For privacy that survives at runtime, use a native private field with a # prefix, like #balance.
Q: Why must I call super() before using this in a subclass?
Because the parent's constructor is what actually sets up the object's inherited fields. Until super() runs, this isn't fully built, so JavaScript forbids touching it.
Mini-Challenge: Build a Stack
No blanks this time — just a brief and an outline. Write the Stack class yourself (a "last in, first out" list), then uncomment the test lines and check your output against the expected lines. This is the kind of small, reusable class real apps are full of.
🎯 Mini-Challenge: a Stack class
Build push, pop, peek, and a size getter from the outline.
// 🎯 MINI-CHALLENGE: a Stack
console.log("=== Stack ===");
// 1. Make a class "Stack" whose constructor sets this.items = [];
// 2. push(item): add item to the end of this.items
// 3. pop(): remove and RETURN the last item
// 4. peek(): return the last item WITHOUT removing it
// 5. get size(): a GETTER returning this.items.length
//
// Then run the test lines at the bottom — they should print the
// expected output below.
// your code here
// ---- test (uncomment once your class exists) -
...🎉 Lesson Complete
- ✅ A class is a blueprint; the constructor builds each object on
this - ✅ Parameter properties declare and assign a field in one line
- ✅
public/private/protected/readonlycontrol access - ✅ Getters & setters run code behind a property — perfect for computed values and validation
- ✅
implementsproves a class fulfils an interface's contract - ✅
extends+supergive inheritance;<T>makes a class generic - ✅ Next lesson: Advanced Types — unions, intersections, and mapped types
Sign up for free to track which lessons you've completed and get learning reminders.