Chapter 14: TypeScript Classes
TypeScript Classes like we’re sitting in a quiet room with a whiteboard, coffee, and no hurry. I’ll explain everything slowly, show patterns people actually use in 2025–2026 real projects, highlight what’s safe / common / dangerous, and give many practical examples.
1. Quick reality check first (very important)
TypeScript classes = JavaScript classes + type annotations + access modifiers + a few extra checks
- At runtime → exactly the same as normal ES2015+ classes (no extra code for private fields in most cases)
- TypeScript adds:
- Type checking for properties & methods
- public / private / protected (some become real #private at runtime)
- readonly
- Parameter properties (shorthand)
- Implements / extends with interface checks
- Better constructor / field initialization rules (especially with strictPropertyInitialization)
2. The most basic class (start here)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Car { // Field (property) declaration brand: string; // must be initialized later year: number = 2025; // default value readonly vin: string; // can set only in constructor // Constructor (special method called with `new`) constructor(brand: string, vin: string) { this.brand = brand; this.vin = vin; // readonly → only here or at declaration } // Method drive(km: number): string { return `${this.brand} drove ${km} km`; } } const myCar = new Car("Toyota", "1HGCM82633A004352"); console.log(myCar.drive(150)); // "Toyota drove 150 km" // myCar.vin = "new"; // Error — readonly! |
3. Parameter properties — very common shorthand (you’ll see this everywhere)
Instead of writing this.xxx = xxx manually:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Person { constructor( public name: string, // ← public + assign automatically private age: number, protected readonly id: number ) { // No need to write this.name = name; etc. } getInfo(): string { // this.age → accessible inside class return `${this.name} (${this.age} years)`; } } const alice = new Person("Alice", 28, 1001); // alice.age → Error (private) // alice.id → Error (protected + readonly) |
Access modifiers quick table (2025–2026 reality)
| Modifier | Accessible from | Real JS output (most targets) | Common use case |
|---|---|---|---|
| (no keyword) | everywhere | normal property | Default — public API |
| public | everywhere | same | Explicit public (some teams prefer) |
| private | only inside class | #private (when target ≥ es2022) or WeakMap | Internal state |
| protected | class + subclasses | #protected or WeakMap | Inheritance — give to children |
| readonly | read, but no re-assign | readonly in .d.ts | IDs, configs, immutable fields |
4. Inheritance (extends) — classic OOP pattern
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Animal { constructor(public name: string) {} move(distance: number = 0) { console.log(`${this.name} moved ${distance}m.`); } } class Dog extends Animal { bark() { console.log(`${this.name} says Woof!`); } // Override method move(distance = 5) { console.log("Dog is running..."); super.move(distance); // call parent version } } const dog = new Dog("Buddy"); dog.bark(); // Buddy says Woof! dog.move(); // Dog is running... Buddy moved 5m. |
5. Abstract classes (very useful for defining contracts)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
abstract class Department { constructor(public name: string) {} // Must be implemented by child classes abstract printMeeting(): void; // Can have normal methods printName() { console.log(`Department: ${this.name}`); } } class AccountingDepartment extends Department { printMeeting() { console.log("Accounting meeting at 10 AM"); } } // const d = new Department("Sales"); // Error — abstract class const dept = new AccountingDepartment("Accounting"); dept.printMeeting(); |
6. Implementing interfaces (very common in real apps)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
interface Printable { print(): string; } interface HasPrice { getPrice(): number; } class Book implements Printable, HasPrice { constructor(private title: string, private price: number) {} print() { return `Book: ${this.title}`; } getPrice() { return this.price; } } |
Many real projects use multiple implements — especially domain entities + DTOs.
7. Static members (belong to class, not instance)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class MathUtils { static PI = 3.14159; static circleArea(radius: number): number { return this.PI * radius * radius; } } console.log(MathUtils.PI); // 3.14159 console.log(MathUtils.circleArea(5)); // ~78.54 // const utils = new MathUtils(); // usually no need to instantiate |
8. Modern patterns you see in 2025–2026 codebases
Pattern A: Private fields + getters/setters
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class User { #passwordHash: string; // real private field (JS output) constructor(public email: string, password: string) { this.#passwordHash = this.hash(password); } private hash(pw: string): string { return btoa(pw); // fake example } get isAdmin(): boolean { return this.email.endsWith("@admin.com"); } set password(newPw: string) { this.#passwordHash = this.hash(newPw); } } |
Pattern B: Readonly + constructor assignment
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Config { readonly apiUrl: string; readonly timeout: number = 5000; constructor(apiUrl: string, timeout?: number) { this.apiUrl = apiUrl; if (timeout !== undefined) this.timeout = timeout; } } |
Pattern C: Using classes with React (classic class component style — still exists)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
interface Props { initialCount: number; } class Counter extends React.Component<Props, { count: number }> { state = { count: this.props.initialCount }; increment = () => { this.setState(prev => ({ count: prev.count + 1 })); }; render() { return ( <div> Count: {this.state.count} <button onClick={this.increment}>+</button> </div> ); } } |
9. Strict mode gotchas (very important — enable strict!)
With “strict”: true or “strictPropertyInitialization”: true:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Point { x: number; // Error! Property 'x' has no initializer... y!: number; // ! = definite assignment assertion (I promise to set it) constructor(x: number) { this.x = x; // ok now } } |
Many teams use ! sparingly — prefer default values or constructor init.
Quick cheat-sheet table
| Feature | Syntax example | Purpose / When to use |
|---|---|---|
| Parameter property | constructor(public name: string) | Reduce boilerplate — very common |
| Private field | private balance: number or #balance | Hide internal state |
| Protected | protected log() | Share with subclasses only |
| Readonly | readonly id: string | Prevent accidental mutation |
| Abstract class | abstract class Shape { abstract area(): number } | Define base contract |
| Static | static createDefault() | Factory methods, constants |
| Implements | class User implements IUser | Enforce shape (very frequent with interfaces) |
| Getter / Setter | get fullName() {} | Computed properties |
Your mini homework (try in playground)
- Create class Employee with public name, private salary, protected department
- Add raiseSalary(percent: number) method that only works inside class
- Create class Manager extends Employee that can see department
- Make salary readonly after constructor
Any part confusing? Want to zoom in on:
- Private vs #private differences
- Abstract classes + templates
- Classes vs interfaces (when to choose what)
- Real React class vs function components with types
- Common mistakes people still make in 2026
Just say the word — we’ll go deeper right there! 😄
