← Курс/Статические члены классов#167 из 257+20 XP

Статические члены классов

static: принадлежит классу, не экземпляру

Статические члены существуют в единственном экземпляре — на уровне класса. К ним обращаются через имя класса, а не через экземпляр.

class MathUtils {
  static readonly PI = 3.14159265358979

  static circleArea(r: number): number {
    return MathUtils.PI * r * r
  }

  static clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max)
  }
}

// Вызов через класс, не через экземпляр:
console.log(MathUtils.PI)              // 3.14159...
console.log(MathUtils.circleArea(5))   // 78.53...
console.log(MathUtils.clamp(15, 0, 10)) // 10

// const m = new MathUtils()
// m.circleArea(5)  // Ошибка TS: Property 'circleArea' does not exist on instance

Статические поля — общее состояние

class User {
  static count = 0
  static readonly maxUsers = 1000

  readonly id: number
  name: string

  constructor(name: string) {
    if (User.count >= User.maxUsers) {
      throw new Error('Достигнут лимит пользователей')
    }
    User.count++
    this.id = User.count
    this.name = name
  }

  static reset() {
    User.count = 0
  }
}

new User('Алексей')  // User.count = 1
new User('Ольга')    // User.count = 2
console.log(User.count)  // 2 — общее для всех экземпляров

Паттерн Singleton

Статические члены идеально подходят для паттерна Singleton — гарантируют, что существует ровно один экземпляр:

class Database {
  private static instance: Database | null = null
  private connected = false

  private constructor(private url: string) {}   // приватный конструктор!

  static getInstance(url: string): Database {
    if (!Database.instance) {
      Database.instance = new Database(url)
    }
    return Database.instance
  }

  connect() {
    if (!this.connected) {
      console.log(`Подключение к ${this.url}`)
      this.connected = true
    }
  }
}

const db1 = Database.getInstance('postgres://localhost:5432/mydb')
const db2 = Database.getInstance('postgres://other-host')  // та же строка игнорируется
console.log(db1 === db2)  // true — один и тот же экземпляр

Static blocks (ES2022)

TypeScript поддерживает статические блоки инициализации:

class Config {
  static readonly defaults: Record<string, string>
  static readonly version: string

  static {
    // Сложная инициализация статических полей
    Config.defaults = { lang: 'ru', theme: 'light', timeout: '5000' }
    Config.version  = process.env.APP_VERSION ?? '1.0.0'
    console.log('Config инициализирован')
  }
}

Static vs Instance — сравнение

class Counter {
  // Статические — общие для всех
  static totalCreated = 0
  static totalDestroyed = 0

  // Экземплярные — у каждого свои
  private count = 0

  constructor() {
    Counter.totalCreated++
  }

  increment() { this.count++ }
  getValue()  { return this.count }

  destroy() { Counter.totalDestroyed++ }

  static getActiveCount() {
    return Counter.totalCreated - Counter.totalDestroyed
  }
}

const c1 = new Counter()
const c2 = new Counter()
c1.increment()
c1.increment()
c2.increment()

console.log(c1.getValue())          // 2 — только у c1
console.log(c2.getValue())          // 1 — только у c2
console.log(Counter.totalCreated)   // 2 — общее

Примеры

Паттерн Singleton и фабрика объектов с использованием статических полей: реестр соединений с БД

// Singleton через статические поля — один экземпляр на всё приложение

class ConnectionPool {
  static #instance = null
  static #defaultConfig = {
    host:     'localhost',
    port:     5432,
    maxSize:  10,
    timeout:  5000,
  }

  #connections = []
  #config
  #stats = { created: 0, released: 0, failed: 0 }

  // Приватный конструктор — нельзя вызвать напрямую
  constructor(config) {
    this.#config = { ...ConnectionPool.#defaultConfig, ...config }
  }

  // Единственная точка доступа к экземпляру
  static getInstance(config = {}) {
    if (!ConnectionPool.#instance) {
      ConnectionPool.#instance = new ConnectionPool(config)
      console.log('[Pool] Создан новый пул соединений')
    }
    return ConnectionPool.#instance
  }

  static reset() {
    ConnectionPool.#instance = null
    console.log('[Pool] Пул сброшен')
  }

  // Получить соединение из пула
  acquire() {
    if (this.#connections.length < this.#config.maxSize) {
      const conn = {
        id: ++this.#stats.created,
        host: this.#config.host,
        port: this.#config.port,
        createdAt: Date.now(),
        busy: true,
      }
      this.#connections.push(conn)
      console.log(`[Pool] Соединение #${conn.id} создано`)
      return conn
    }
    throw new Error(`Пул заполнен (max: ${this.#config.maxSize})`)
  }

  // Вернуть соединение в пул
  release(conn) {
    const idx = this.#connections.findIndex(c => c.id === conn.id)
    if (idx === -1) throw new Error('Соединение не принадлежит пулу')
    this.#connections.splice(idx, 1)
    this.#stats.released++
    console.log(`[Pool] Соединение #${conn.id} освобождено`)
  }

  get stats() {
    return {
      ...this.#stats,
      active: this.#connections.length,
      config: { ...this.#config },
    }
  }
}

// Registry — статический реестр через Map
class ServiceRegistry {
  static #services = new Map()
  static #count = 0

  static register(name, factory) {
    if (ServiceRegistry.#services.has(name)) {
      throw new Error(`Сервис "${name}" уже зарегистрирован`)
    }
    ServiceRegistry.#services.set(name, { factory, count: 0 })
    ServiceRegistry.#count++
    console.log(`[Registry] Зарегистрирован: ${name}`)
  }

  static get(name) {
    const entry = ServiceRegistry.#services.get(name)
    if (!entry) throw new Error(`Сервис "${name}" не найден`)
    entry.count++
    return entry.factory()
  }

  static get registeredCount() { return ServiceRegistry.#count }

  static list() { return [...ServiceRegistry.#services.keys()] }
}

// --- Демонстрация ---
console.log('=== Singleton: ConnectionPool ===')
const pool1 = ConnectionPool.getInstance({ host: 'db.prod.com', maxSize: 5 })
const pool2 = ConnectionPool.getInstance({ host: 'другой хост' })  // игнорируется

console.log('Это один экземпляр:', pool1 === pool2)  // true

const conn1 = pool1.acquire()
const conn2 = pool1.acquire()
console.log('Активных соединений:', pool1.stats.active)  // 2

pool1.release(conn1)
console.log('После release:', pool1.stats)

console.log('\n=== Статический реестр сервисов ===')
ServiceRegistry.register('logger',    () => ({ log: console.log }))
ServiceRegistry.register('formatter', () => ({ format: (s) => s.toUpperCase() }))

console.log('Зарегистрировано:', ServiceRegistry.registeredCount)  // 2
console.log('Список:', ServiceRegistry.list())

const logger = ServiceRegistry.get('logger')
logger.log('Сервис получен из реестра')

try {
  ServiceRegistry.register('logger', () => {})
} catch (e) {
  console.log('Ошибка:', e.message)
}