Classes and Objects

Reviewed & published by Brayan K

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.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

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.

// 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.name + " (" + this.age + ")"; }
}

// "new User(...)" calls the constructor and hands you a fresh object.
let alice = new User("Alice", "[email protected]", 28);
let bob   = new User("Bob", "[email protected]", 34);

console.log(alice.greet());   // Hi, I'm Alice (28)
console.log(bob.greet());     // Hi, I'm Bob (34)
console.log("alice.email = " + alice.email);  // [email protected]

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-only

3. 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.

// 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("Below absolute zero!");
    this._celsius = value;
  }
}

let temp = new Temperature(25);
console.log("Celsius:    " + temp.celsius);     // 25   (no () — it's a getter)
console.log("Fahrenheit: " + temp.fahrenheit);  // 77

temp.celsius = 30;                               // runs the setter
console.log("After set:  " + temp.fahrenheit);   // 86

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 — 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 b = new Book("Dune", "Herbert", 412);
console.log(b.describe());

// ✅ Expected output:
//    === Build a Book ===
//    Dune by Herbert (412 pages)

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.

// 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 Circle-specific state
  }
  // Override: replace the parent's area() with a real one
  area() { return Math.round(Math.PI * this.radius ** 2 * 100) / 100; }
}

class Rectangle extends Shape {
  constructor(width, height, color) {
    super("rectangle", color);
    this.width = width;
    this.height = height;
  }
  area() { return this.width * this.height; }
}

// Each shape uses ITS OWN area(), but shares describe() from Shape:
let shapes = [new Circle(5, "red"), new Rectangle(4, 6, "blue")];
for (let s of shapes) {
  console.log(s.describe() + " → area = " + s.area());
}
// red circle → area = 78.54
// blue rectangle → area = 24

Now you try. Make Dog inherit from Animal, call super in its constructor, and override speak(). Fill in the four blanks:

// 🎯 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 + " barks"
  speak() { return this.name + ___; }   // 👉 " barks"
}

let rex = new Dog("Rex", "Labrador");
console.log(rex.speak());
console.log(rex.name + " is a " + rex.breed);

// ✅ Expected output:
//    === Animals ===
//    Rex barks
//    Rex is a Labrador

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; }
}
// 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:
  compareTo(other) { return this.price - other.price; }
}

let items = [new Product("Laptop", 999), new Product("Phone", 699), new Product("Tablet", 449)];

// Because every Product has compareTo, we can sort with it:
items.sort((a, b) => a.compareTo(b));
console.log("Cheapest first:");
for (let p of items) { console.log("  $" + p.price + " — " + p.name); }
// $449 — Tablet
// $699 — Phone
// $999 — Laptop

🔎 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 number

You'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)

Pro Tips

📋 Quick Reference

FeatureSyntaxMeaning
Typed fieldname: string;A field that must hold text
Param propertyconstructor(public x: number)Declares & assigns in one line
Privateprivate balance: numberOnly inside this class
Protectedprotected pin: stringThis class + subclasses
Readonlyreadonly id: stringSet once, then locked
Getterget area() { ... }Runs code on property read
Implementsclass X implements Y, ZPromise to fulfil a contract
Extendsclass Dog extends AnimalInherit from a parent
Generic classclass 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
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) ----
// let s = new Stack();
// s.push("a"); s.push("b"); s.push("c");
// console.log("size:  " + s.size);   // size:  3
// console.log("peek:  " + s.peek()); // peek:  c
// console.log("pop:   " + s.pop());  // pop:   c
// console.log("size:  " + s.size);   // size:  2

// ✅ Expected output:
//    === Stack ===
//    size:  3
//    peek:  c
//    pop:   c
//    size:  2

🎉 Lesson Complete

Practice quiz

What is the job of a class constructor?

  • To define the class name
  • To delete the object
  • To run once on new ClassName(...) and set up the new object on this
  • To import dependencies

Answer: To run once on new ClassName(...) and set up the new object on this. The constructor runs once when you write new ClassName(...); its job is to store starting values on this, the new object.

What does a parameter property like constructor(public name: string) do?

  • Declares the field AND assigns it in one line
  • Makes the parameter optional
  • Marks the constructor as public
  • Creates a getter

Answer: Declares the field AND assigns it in one line. Putting an access modifier on a constructor parameter declares the field and assigns it automatically - no this.name = name needed.

Which access modifier limits a field to only inside the same class?

  • public
  • protected
  • readonly
  • private

Answer: private. private restricts a field to the class itself. protected also allows subclasses; public is accessible everywhere.

What does protected add compared to private?

  • Access from anywhere
  • Access from this class AND its subclasses
  • Runtime immutability
  • Nothing - they are identical

Answer: Access from this class AND its subclasses. protected widens private's circle to include subclasses, but still blocks access from outside the class hierarchy.

Where can a readonly field be assigned?

  • Only in the constructor, then it's locked
  • Anywhere in the class
  • Only outside the class
  • Never

Answer: Only in the constructor, then it's locked. A readonly field can be set once - in the constructor - and is then frozen; later assignment is a compile error (TS2540).

Is TypeScript's private modifier enforced at runtime?

  • Yes, the field is hidden in compiled JavaScript
  • Only in strict mode
  • No - it's a compile-time check only; use #field for runtime privacy
  • Only for methods

Answer: No - it's a compile-time check only; use #field for runtime privacy. private is compile-time only; the compiled JS still exposes the field. Use a native #field for privacy JavaScript enforces.

How is a getter accessed?

  • Like a method call: temp.fahrenheit()
  • Like a plain property, with no parentheses: temp.fahrenheit
  • With the get keyword each time
  • Only via a setter

Answer: Like a plain property, with no parentheses: temp.fahrenheit. A getter looks like a property when read - temp.fahrenheit with no parentheses - but actually runs code behind the scenes.

In a subclass constructor, what must you do before accessing this?

  • Declare all fields
  • Return a value
  • Call the method being overridden
  • Call super(...) first

Answer: Call super(...) first. You must call super(...) before touching this, because the parent's constructor sets up the inherited part of the object.

What does class Product implements Comparable tell TypeScript?

  • Product inherits Comparable's code
  • Product must provide every member Comparable requires, checked at compile time
  • Comparable becomes a subclass of Product
  • Product is converted to an interface

Answer: Product must provide every member Comparable requires, checked at compile time. implements is a contract: TypeScript verifies the class supplies every member the interface lists, erroring with TS2420 if not.

What does a generic class Box<T> give you?

  • A box that holds only strings
  • A class with no fields
  • One class that works with any type while staying type-safe
  • A runtime type check

Answer: One class that works with any type while staying type-safe. A type parameter <T> lets one class work with any type safely - a Box<number> only holds numbers, a Box<string> only strings.

Continue this course