Как типизировать классы и ООП в TypeScript?
Вопрос
Как работают модификаторы доступа (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-поля на совместимость двух разных классов?