Skip to content

07 · Classes Basics

🎥 Video walkthrough

TypeScript classes look like JavaScript classes with typed properties, typed constructor parameters, and access modifiers the compiler actually enforces.

Defining a class with typed properties

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): string {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
  }
}

const ada = new Person("Ada", 30);
console.log(ada.greet());   // Hi, I'm Ada and I'm 30 years old.

Parameter properties — a shorthand constructor

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const grace = new Person("Grace", 40);
console.log(grace.greet());   // Hi, I'm Grace

Prefixing a constructor parameter with public, private, or protected declares AND assigns the property in one step — no separate this.name = name line needed.

Access modifiers

class BankAccount {
  private balance: number;

  constructor(public owner: string, initialBalance: number) {
    this.balance = initialBalance;
  }

  deposit(amount: number): void {
    this.balance += amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount("Ada", 100);
account.deposit(50);
console.log(account.getBalance());   // 150
// console.log(account.balance);      // error TS2341: Property 'balance' is private
Modifier Visible from
public (default) Anywhere
private Only inside this class
protected This class and subclasses
readonly Anywhere, but can't be reassigned after construction

Inheritance with extends

class Animal {
  constructor(public name: string) {}

  makeSound(): string {
    return "...";
  }
}

class Dog extends Animal {
  makeSound(): string {
    return "Woof!";
  }
}

class Cat extends Animal {
  makeSound(): string {
    return "Meow!";
  }
}

const animals: Animal[] = [new Dog("Rex"), new Cat("Whiskers")];

for (const animal of animals) {
  console.log(`${animal.name}: ${animal.makeSound()}`);
}
// Rex: Woof!
// Whiskers: Meow!

Calling the parent constructor with super

class Animal {
  constructor(public name: string) {}
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);   // must call super() before using `this` in a subclass
  }
}

const rex = new Dog("Rex", "Labrador");
console.log(`${rex.name} is a ${rex.breed}`);   // Rex is a Labrador

Getters and setters

class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Below absolute zero");
    }
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 9 / 5 + 32;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit);   // 77

get/set let you use property-like syntax (temp.celsius = 25) while still running validation logic behind the scenes.

How It Actually Works

TypeScript classes compile to plain JS class syntax (or, on older targets, to constructor-function-plus-prototype patterns), with every type annotation stripped: private, public, protected, parameter-property shorthand, and any : Type annotation on a field all vanish from the emitted code. This means private in TypeScript is a compile-time-only access restriction — the checker refuses code outside the class that reads instance.secret, but the emitted JS field is an ordinary, fully-accessible property, so instance["secret"] (bracket access with a string) or plain JS calling code bypasses the restriction entirely at runtime. True runtime privacy requires the JS-native #field syntax, which TypeScript also supports and which does enforce access at the engine level because it's a distinct kind of property key, not compiler-checked.

Despite being structurally typed everywhere else, TypeScript classes are compared structurally, with one nominal-like exception: two classes with identical public members are mutually assignable even if unrelated by inheritance — but a private or protected member makes the comparison behave nominally for that pair, because the checker additionally requires the member to originate from the same declaration (the same class or a shared ancestor), not just a matching name and type. This is why two classes that both declare a private id: string field are not assignable to each other even though the public shape looks identical — the checker treats each class's private members as tagged with that specific class's identity.

Parameter properties (constructor(private name: string) {}) are pure syntactic sugar: the compiler expands that single parameter into a declared class field plus an assignment statement (this.name = name;) inserted at the top of the constructor body — nothing new is introduced with respect to private's erasure, it's just fewer characters to type; the underlying execution is identical to writing the field and the assignment out by hand.

Cheat sheet

Feature Syntax
Class class Name { ... }
Constructor shorthand constructor(public x: T) {}
Access modifiers public / private / protected / readonly
Inheritance class Sub extends Base { ... }
Call parent constructor super(args)
Getter/setter get prop() { ... } / set prop(v) { ... }

🔀 See this in another language

Exercise

Write a class Shape with a protected method area(): number that returns 0 by default. Create subclasses Circle (constructor takes a radius) and Rectangle (constructor takes width and height), each overriding area() correctly. Put instances of both in a Shape[] array and print each one's area using a loop.