Типизация событий

Система событий в Tom Select построена вокруг подписочной модели, где поведение компонента описывается набором именованных событий, вызываемых в разные моменты жизненного цикла экземпляра. Архитектурно это приближает библиотеку к событийно-ориентированным UI-компонентам: любое изменение состояния транслируется наружу через on(...) и может быть перехвачено без вмешательства во внутреннюю реализацию.

При переходе к TypeScript или строгой типизации основной сложностью становится отсутствие встроенной типовой модели событий в ранних версиях API. В результате типизация событий опирается на сопоставление строковых идентификаторов событий и структур их payload-данных.


Базовые события и их семантика

Внутренний набор событий охватывает ключевые операции:

  • добавление и удаление элементов
  • изменение значения
  • открытие и закрытие dropdown
  • загрузка и фильтрация опций
  • создание новых элементов

Типичный набор событий:

  • change
  • item_add
  • item_remove
  • dropdown_open
  • dropdown_close
  • type
  • load
  • initialize

Каждое событие имеет собственную сигнатуру аргументов, но в JavaScript-версии они не формализованы на уровне типов.

Пример без типизации:

select.on('item_add', function (value, item) {
  console.log(value);
  console.log(item);
});

Проблема отсутствия строгой типизации событий

При использовании TypeScript возникает фундаментальная проблема: события представлены строками, а их payload — вариативными аргументами функции.

Основные сложности:

  • отсутствует единый EventMap
  • разные события имеют разное количество аргументов
  • часть событий зависит от конфигурации (например, create)
  • плагины добавляют новые события динамически

Это делает невозможным простое описание типа вроде:

on(event: string, handler: Function): void;

Такой подход приводит к потере информации о структуре данных.


Формирование Event Map

Базовый способ типизации — введение отображения событий в виде интерфейса.

interface TomSelectEvents {
  change: (value: string) => void;
  item_add: (value: string, item: HTMLElement) => void;
  item_remove: (value: string, item: HTMLElement) => void;
  dropdown_open: () => void;
  dropdown_close: () => void;
  load: (options: any[]) => void;
}

Далее используется обобщённая сигнатура:

type EventKey = keyof TomSelectEvents;

type EventHandler<K extends EventKey> = TomSelectEvents[K];

Типизация метода on

Основная точка интеграции событий — метод регистрации обработчиков.

class TypedTomSelect {
  on<K extends keyof TomSelectEvents>(
    event: K,
    handler: TomSelectEvents[K]
  ): void {}
}

Это позволяет связать строковый ключ события с конкретной сигнатурой обработчика.


Типизация payload событий

Событие добавления элемента

item_add: (value: string, item: HTMLElement) => void;

Семантика:

  • value — значение опции
  • item — DOM-узел, созданный для элемента

Типизация DOM-элементов особенно важна, поскольку внутри библиотеки они могут быть либо HTMLElement, либо специализированные расширения.


Событие изменения значения

change: (value: string) => void;

Особенность заключается в том, что событие может агрегировать несколько значений при multiple: true. В таком случае строгая типизация расширяется:

change: (value: string | string[]) => void;

События dropdown

dropdown_open: () => void;
dropdown_close: () => void;

Отсутствие payload позволяет выразить такие события как void-сигнатуры, что упрощает проверку корректности обработчиков.


Расширение событий через плагины

Tom Select поддерживает плагины, которые могут добавлять собственные события. Это создаёт проблему расширяемости EventMap.

Решение в TypeScript — декларативное расширение интерфейса:

interface TomSelectEvents {
  plugin_custom_event: (data: { id: number }) => void;
}

Или через module augmentation:

declare module "tom-select" {
  interface TomSelectEvents {
    virtual_scroll: (state: boolean) => void;
  }
}

Универсальный тип событий (fallback)

В случаях, когда невозможно заранее определить структуру событий, используется fallback-типизация:

type AnyEventHandler = (...args: any[]) => void;

class LooseTomSelect {
  on(event: string, handler: AnyEventHandler): void {}
}

Недостаток такого подхода — полная потеря контекста аргументов, но сохранение совместимости с динамическими плагинами.


Типизация метода off и once

Событийная модель включает управление жизненным циклом подписок:

off<K extends keyof TomSelectEvents>(
  event: K,
  handler: TomSelectEvents[K]
): void;

once<K extends keyof TomSelectEvents>(
  event: K,
  handler: TomSelectEvents[K]
): void;

Особенность once заключается в автоматическом удалении обработчика после первого вызова, что не требует дополнительных типов, но важно учитывать при анализе поведения.


Сопоставление событий с внутренними состояниями

Типизация событий часто опирается на внутренние state transitions:

Состояние Событие
выбор элемента item_add
удаление item_remove
ввод текста type
загрузка данных load
открытие UI dropdown_open

Такое сопоставление позволяет формировать более строгие типы через discriminated mapping:

type StateEvent =
  | { type: "item_add"; value: string }
  | { type: "item_remove"; value: string }
  | { type: "change"; value: string };

Типизация контекста this в событиях

В обработчиках событий контекстом часто выступает экземпляр компонента. В TypeScript это выражается через явное указание this:

item_add: (this: TypedTomSelect, value: string, item: HTMLElement) => void;

Такой подход позволяет внутри обработчиков использовать методы экземпляра без потери типизации.


Интеграция с generics

Для более строгой модели используется обобщённый класс:

class TypedTomSelect<TEvents extends Record<string, any>> {
  on<K extends keyof TEvents>(
    event: K,
    handler: TEvents[K]
  ): void {}
}

Это позволяет полностью переопределять набор событий для конкретной конфигурации экземпляра.


Типизация асинхронных событий

Некоторые события, такие как load, имеют асинхронную природу:

load: (options: Option[]) => void;

В расширенной модели они могут возвращать Promise:

load: (options: Option[]) => Promise<void>;

Это особенно важно при использовании remote data sources.


Динамическая генерация событий и ограничения типизации

Внутренняя архитектура Tom Select допускает динамическое добавление событий через плагины и пользовательские расширения. Это приводит к нескольким ограничениям:

  • невозможность полного статического анализа
  • необходимость fallback-типов
  • частичная потеря строгой проверки при runtime-расширениях

По этой причине типизация событий часто строится как гибридная модель:

  • строгие базовые события
  • расширяемые плагины через augmentation
  • динамические события через string index signature

Композиционная модель событий

Для сложных интеграций используется композиция EventMap:

type BaseEvents = TomSelectEvents;

type ExtendedEvents = BaseEvents & {
  custom_filter: (query: string) => void;
};

Такой подход сохраняет совместимость с базовой библиотекой и расширяет событийную модель без изменения ядра.


Итоговая структура типизации событий

В зрелой модели событийная система сводится к трём уровням:

  • строгие базовые события ядра
  • расширения через declaration merging
  • динамические события через универсальные обработчики

Эта структура отражает баланс между строгой типизацией и гибкостью архитектуры Tom Select, где событийная модель остаётся расширяемой без потери базовой типовой целостности.