Как типизировать классы и ООП в TypeScript?

MiddleTypeScript #typescript #classes #oop

Вопрос

Как работают модификаторы доступа (public/private/protected), абстрактные классы и в чём разница между implements и extends?

Короткий ответ

public (по умолчанию), private и protected управляют видимостью членов класса: private — только внутри класса, protected — внутри класса и наследников, public — везде. abstract class — класс, который нельзя инстанцировать напрямую, задающий контракт для наследников. extends — наследование реализации от одного класса, implements — обязательство реализовать структуру интерфейса без наследования кода.

Подробный ответ

Модификаторы доступа — это проверка только на этапе компиляции; в скомпилированном JS они никак не ограничивают доступ в рантайме (в отличие от настоящих приватных полей #field, которые обеспечивают приватность и в рантайме):

class BankAccount {
  private balance: number
  protected owner: string
  public readonly accountId: string

  constructor(owner: string, initial: number) {
    this.owner = owner
    this.balance = initial
    this.accountId = crypto.randomUUID()
  }

  private validate(amount: number) {
    if (amount <= 0) throw new Error('Invalid amount')
  }

  deposit(amount: number) {
    this.validate(amount) // OK — доступ внутри класса
    this.balance += amount
  }
}

const acc = new BankAccount('Alice', 100)
// acc.balance // Ошибка — private недоступен снаружи
// acc.validate(10) // Ошибка — private метод

Есть также сокращённый синтаксис — parameter properties, объявляющий и типизирующий поле прямо в конструкторе:

class Point {
  constructor(public x: number, public y: number) {}
}
// эквивалентно объявлению полей x, y и присвоению их в теле конструктора

abstract class — базовый класс, который нельзя создать через new напрямую. Может содержать как реализованные методы, так и abstract-методы без тела, обязательные к реализации в наследниках:

abstract class Shape {
  abstract area(): number // без реализации — контракт

  describe(): string {    // с реализацией — общий для всех наследников
    return `Area: ${this.area()}`
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super() }
  area(): number { return Math.PI * this.radius ** 2 }
}

// const s = new Shape() // Ошибка — нельзя инстанцировать abstract класс

extends vs implements:

  • extends — наследование от класса (или интерфейса, для интерфейсов). Наследник получает реализацию родителя (поля, методы) и может её переопределять, обязан вызвать super() в конструкторе, если родитель имеет конструктор.
  • implements — класс обязуется соответствовать структуре интерфейса (или нескольких через запятую), но не получает от него никакого кода — реализацию нужно писать полностью самому. Это чисто проверка контракта на этапе компиляции.
interface Flyable { fly(): void }
interface Swimmable { swim(): void }

class Duck extends Animal implements Flyable, Swimmable {
  fly() { console.log('Flying') }
  swim() { console.log('Swimming') }
}

Класс может реализовывать несколько интерфейсов (implements A, B), но наследовать только от одного класса (extends — одиночное наследование).

Пример

abstract class Repository<T extends { id: number }> {
  protected items: T[] = []

  abstract validate(item: T): boolean

  add(item: T): void {
    if (!this.validate(item)) throw new Error('Invalid item')
    this.items.push(item)
  }

  findById(id: number): T | undefined {
    return this.items.find((i) => i.id === id)
  }
}

interface Serializable {
  toJSON(): string
}

interface Product { id: number; name: string; price: number }

class ProductRepository extends Repository<Product> implements Serializable {
  validate(item: Product): boolean {
    return item.price >= 0 && item.name.length > 0
  }

  toJSON(): string {
    return JSON.stringify(this.items)
  }
}

const repo = new ProductRepository()
repo.add({ id: 1, name: 'Book', price: 19.99 })
console.log(repo.toJSON())

Дополнительные вопросы

  • Чем настоящие приватные поля класса (#field) отличаются от модификатора private?
  • Можно ли объявить abstract свойство (не метод) в абстрактном классе?
  • Как TypeScript проверяет совместимость классов при структурной типизации — влияют ли private-поля на совместимость двух разных классов?