← Курс/Классы в TypeScript#158 из 257+30 XP

Классы в TypeScript

Модификаторы доступа

TypeScript добавляет к классам JavaScript три модификатора доступа:

class Person {
  public name: string      // доступен везде (по умолчанию)
  private age: number      // только внутри класса
  protected email: string  // внутри класса и в подклассах

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

  getInfo(): string {
    return `${this.name}, возраст ${this.age}`  // private доступен внутри
  }
}

const p = new Person('Алексей', 30, 'alex@mail.ru')
console.log(p.name)    // OK — public
// p.age               // Ошибка TS: Property 'age' is private
// p.email             // Ошибка TS: Property 'email' is protected

Краткая запись параметров конструктора

TypeScript позволяет объявить и инициализировать поля прямо в сигнатуре конструктора:

// Длинная запись:
class User {
  private name: string
  private age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

// Короткая запись — то же самое:
class User {
  constructor(
    private name: string,
    private age: number
  ) {}
}

Модификатор в параметре конструктора = объявление поля + присваивание в одну строку.

readonly

Поле можно задать только при создании, дальше — только чтение:

class Config {
  readonly apiUrl: string
  readonly version: string = '1.0.0'  // можно с дефолтом

  constructor(apiUrl: string) {
    this.apiUrl = apiUrl  // OK — в конструкторе можно
  }

  update() {
    // this.apiUrl = '...'  // Ошибка TS: Cannot assign to 'apiUrl' because it is a read-only property
  }
}

Getter и Setter

class Temperature {
  private _celsius: number

  constructor(celsius: number) {
    this._celsius = celsius
  }

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

  set fahrenheit(value: number) {
    this._celsius = (value - 32) * 5/9
  }

  get celsius(): number { return this._celsius }
  set celsius(value: number) {
    if (value < -273.15) throw new RangeError('Ниже абсолютного нуля')
    this._celsius = value
  }
}

const t = new Temperature(100)
console.log(t.fahrenheit)  // 212
t.fahrenheit = 32
console.log(t.celsius)     // 0

Abstract классы и методы

Abstract класс — шаблон, который **нельзя инстанциировать напрямую**. Он определяет контракт для подклассов:

abstract class Shape {
  abstract area(): number          // абстрактный метод — без реализации
  abstract perimeter(): number

  // Обычный метод — с реализацией, наследуется
  toString(): string {
    return `Shape: area=${this.area().toFixed(2)}`
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super() }

  area(): number { return Math.PI * this.radius ** 2 }
  perimeter(): number { return 2 * Math.PI * this.radius }
}

class Rectangle extends Shape {
  constructor(private w: number, private h: number) { super() }

  area(): number { return this.w * this.h }
  perimeter(): number { return 2 * (this.w + this.h) }
}

// const s = new Shape()  // Ошибка TS: Cannot create an instance of an abstract class
const c = new Circle(5)
console.log(c.toString())  // 'Shape: area=78.54'

implements: класс реализует интерфейс

interface Printable {
  print(): void
}

interface Serializable {
  serialize(): string
}

// Класс может реализовать несколько интерфейсов
class Document implements Printable, Serializable {
  constructor(private content: string) {}

  print(): void {
    console.log(this.content)
  }

  serialize(): string {
    return JSON.stringify({ content: this.content })
  }
}

Abstract vs Interface — разница

| Характеристика | abstract class | interface |

|---|---|---|

| Реализация методов | Может иметь | Нет (только типы) |

| Поля с состоянием | Да | Нет |

| Конструктор | Да | Нет |

| Наследование | extends (один) | implements (несколько) |

| Компилируется в JS | Да (класс) | Нет (только типы) |

**Правило**: используй interface для описания формы данных/контракта API, abstract class — когда нужна общая логика с обязательными точками расширения.

Примеры

Иерархия классов: абстрактная фигура Shape, Rectangle, Circle, Triangle с вычислением площадей

// В TypeScript это было бы abstract class Shape
// В JavaScript эмулируем через обычный класс с проверкой в runtime

class Shape {
  constructor(color = 'black') {
    if (new.target === Shape) {
      throw new Error('Shape — абстрактный класс, нельзя создать напрямую')
    }
    this.color = color
  }

  // "Абстрактный" метод — подклассы обязаны переопределить
  area() {
    throw new Error(`${this.constructor.name} должен реализовать area()`)
  }

  perimeter() {
    throw new Error(`${this.constructor.name} должен реализовать perimeter()`)
  }

  // Конкретный метод — наследуется всеми подклассами
  toString() {
    return (
      `[${this.constructor.name}] ` +
      `площадь=${this.area().toFixed(2)}, ` +
      `периметр=${this.perimeter().toFixed(2)}, ` +
      `цвет=${this.color}`
    )
  }

  // TypeScript: implements Comparable
  isLargerThan(other) {
    return this.area() > other.area()
  }
}

class Rectangle extends Shape {
  // TypeScript: constructor(private w: number, private h: number)
  constructor(w, h, color) {
    super(color)
    this.w = w
    this.h = h
  }

  area() { return this.w * this.h }
  perimeter() { return 2 * (this.w + this.h) }
}

class Circle extends Shape {
  constructor(radius, color) {
    super(color)
    this.radius = radius
  }

  area() { return Math.PI * this.radius ** 2 }
  perimeter() { return 2 * Math.PI * this.radius }

  // Getter — как в TypeScript get diameter()
  get diameter() { return this.radius * 2 }
}

class Triangle extends Shape {
  constructor(a, b, c, color) {
    super(color)
    if (a + b <= c || a + c <= b || b + c <= a) {
      throw new Error('Невалидный треугольник')
    }
    this.a = a
    this.b = b
    this.c = c
  }

  perimeter() { return this.a + this.b + this.c }

  area() {
    // Формула Герона
    const s = this.perimeter() / 2
    return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c))
  }
}

// --- Демонстрация ---

const shapes = [
  new Rectangle(10, 5, 'red'),
  new Circle(7, 'blue'),
  new Triangle(3, 4, 5, 'green'),
  new Rectangle(6, 6, 'yellow'),
]

console.log('=== Все фигуры ===')
shapes.forEach(s => console.log(s.toString()))

console.log('\n=== Сортировка по площади (возр.) ===')
const sorted = [...shapes].sort((a, b) => a.area() - b.area())
sorted.forEach(s => console.log(`${s.constructor.name}: ${s.area().toFixed(2)}`))

console.log('\n=== Итого ===')
const totalArea = shapes.reduce((sum, s) => sum + s.area(), 0)
console.log(`Суммарная площадь: ${totalArea.toFixed(2)}`)

const circle = shapes[1]
console.log(`\nДиаметр круга: ${circle.diameter}`)
console.log(`Прямоугольник больше круга? ${shapes[0].isLargerThan(circle)}`)

// Попытка создать Shape напрямую
try {
  new Shape('purple')
} catch (e) {
  console.log(`\nОшибка: ${e.message}`)
}