TL;DR
Learn TypeScript classes and OOP with constructors, inheritance, access modifiers, and interfaces for type-safe object-oriented code.
Key concepts
- TypeScript classes
- TypeScript OOP
- TypeScript inheritance
- access modifiers TypeScript
Classes And OOP
JavaScript has always had objects, but TypeScript takes object-oriented programming (OOP) to a new level by adding access modifiers, strong typing on class members, and first-class interface support. Classes let you bundle data and behavior together into reusable blueprints — and TypeScript ensures you use them correctly at compile time.
Defining a Class
A class is a template for creating objects. It defines properties (data) and methods (behavior) that every instance will have.
class Product {
name: string;
price: number;
inStock: boolean;
constructor(name: string, price: number, inStock = true) {
this.name = name;
this.price = price;
this.inStock = inStock;
}
describe(): string {
const availability = this.inStock ? "In stock" : "Out of stock";
return `${this.name} — $${this.price.toFixed(2)} (${availability})`;
}
}
const shirt = new Product("Merino Wool Shirt", 79.99);
const soldOut = new Product("Limited Hoodie", 129.99, false);
console.log(shirt.describe()); // "Merino Wool Shirt — $79.99 (In stock)"
console.log(soldOut.describe()); // "Limited Hoodie — $129.99 (Out of stock)"
The constructor runs once when you call new Product(...). Properties declared outside the constructor must be initialized either inline or inside it — TypeScript enforces this with strict property initialization checks.
Access Modifiers
TypeScript adds three access modifiers that control where class members can be read or written:
public— accessible everywhere (the default)private— accessible only inside the class itselfprotected— accessible inside the class and its subclasses
class BankAccount {
public owner: string;
private balance: number;
constructor(owner: string, initialBalance: number) {
this.owner = owner;
this.balance = initialBalance;
}
deposit(amount: number): void {
if (amount <= 0) throw new Error("Deposit must be positive");
this.balance += amount;
}
withdraw(amount: number): void {
if (amount > this.balance) throw new Error("Insufficient funds");
this.balance -= amount;
}
getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("Alice", 1000);
account.deposit(500);
account.withdraw(200);
console.log(`${account.owner}'s balance: $${account.getBalance()}`); // "$1300"
// account.balance = 99999; // Error: 'balance' is private
Making balance private means external code can only interact with it through deposit, withdraw, and getBalance — this is the OOP principle of encapsulation.
Constructor Shorthand
TypeScript lets you skip the property declarations and this.x = x assignments by adding access modifiers directly to constructor parameters:
class Point {
constructor(
public x: number,
public y: number
) {}
distanceTo(other: Point): number {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
toString(): string {
return `(${this.x}, ${this.y})`;
}
}
const origin = new Point(0, 0);
const target = new Point(3, 4);
console.log(`Distance from ${origin} to ${target}: ${origin.distanceTo(target)}`);
// "Distance from (0, 0) to (3, 4): 5"
This shorthand is idiomatic TypeScript — you'll see it everywhere in real codebases.
Inheritance
One class can extend another to inherit its properties and methods. The child class can override parent methods to specialize behavior.
class Animal {
constructor(public name: string) {}
speak(): string {
return `${this.name} makes a sound.`;
}
toString(): string {
return this.name;
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // must call super before using 'this'
}
speak(): string {
return `${this.name} barks!`;
}
fetch(item: string): string {
return `${this.name} fetches the ${item}.`;
}
}
class Cat extends Animal {
speak(): string {
return `${this.name} meows.`;
}
}
const dog = new Dog("Rex", "Labrador");
const cat = new Cat("Whiskers");
console.log(dog.speak()); // "Rex barks!"
console.log(dog.fetch("ball")); // "Rex fetches the ball."
console.log(dog.breed); // "Labrador"
console.log(cat.speak()); // "Whiskers meows."
The super() call in the child constructor passes arguments up to the parent. When overriding a method, you can also call super.speak() to invoke the parent's version if you need it.
Implementing Interfaces
Classes can implement one or more interfaces, which act as contracts that guarantee certain properties and methods exist.
interface Serializable {
serialize(): string;
}
interface Validatable {
isValid(): boolean;
}
class EmailAddress implements Serializable, Validatable {
constructor(private address: string) {}
isValid(): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.address);
}
serialize(): string {
return JSON.stringify({ email: this.address });
}
toString(): string {
return this.address;
}
}
const valid = new EmailAddress("user@example.com");
const invalid = new EmailAddress("not-an-email");
console.log(valid.isValid()); // true
console.log(invalid.isValid()); // false
console.log(valid.serialize()); // '{"email":"user@example.com"}'
The implements keyword does not add any runtime behavior — it only tells TypeScript to check that your class satisfies the interface shape. If you forget a required method, TypeScript reports an error at the class definition, not at the call site.
Getters and Setters
TypeScript supports get and set accessors, which look like property access but run a function behind the scenes.
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
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;
}
get kelvin(): number {
return this._celsius + 273.15;
}
}
const temp = new Temperature(100);
console.log(`${temp.celsius}°C = ${temp.fahrenheit}°F = ${temp.kelvin}K`);
// "100°C = 212°F = 373.15K"
temp.celsius = 0;
console.log(`${temp.celsius}°C = ${temp.fahrenheit}°F`);
// "0°C = 32°F"
Getters and setters let you add validation or computed properties while keeping a clean property-style API.
Try It Yourself
Build a small shape hierarchy. Each shape should know its area and perimeter, and be able to describe itself.
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `${this.constructor.name}: area=${this.area().toFixed(2)}, perimeter=${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(public radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
}
class Square extends Rectangle {
constructor(side: number) {
super(side, side);
}
}
const shapes: Shape[] = [
new Circle(5),
new Rectangle(4, 6),
new Square(3),
];
for (const shape of shapes) {
console.log(shape.describe());
}
// Try adding a Triangle class with three sides!
abstract classes cannot be instantiated directly — they exist purely as base classes. Abstract methods must be implemented by every subclass, giving you compile-time enforcement of the contract across the entire hierarchy.
Key Takeaways
- Classes bundle related data and behavior into reusable blueprints;
new ClassName()creates an instance - Access modifiers (
public,private,protected) enforce encapsulation and prevent unintended mutation - Constructor shorthand (
constructor(public x: number)) eliminates boilerplate property declarations - Inheritance (
extends) lets subclasses reuse and specialize parent behavior; always callsuper()first - Interfaces used with
implementsact as contracts, letting you write polymorphic code against a shape rather than a concrete class - Getters and setters add computed properties and validation behind a clean property-style API
- Abstract classes define partial implementations and enforce that subclasses complete the contract
Pro Tip: Prefer composition over inheritance for complex domains. Instead of a deep
Animal → Mammal → Pet → Dogchain, consider giving aDogclass aBehaviorobject that encapsulates its specific traits. TypeScript interfaces make this pattern easy — define the behavior shape as an interface, pass it in the constructor, and keep each class shallow and focused.
Next Steps
Classes often need to do things that take time — fetching data, reading files, waiting for responses. Next, you'll learn how TypeScript types flow through Promises and async/await, and how to handle async errors without losing type safety.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.