Система событий в Tom Select построена вокруг подписочной модели, где
поведение компонента описывается набором именованных событий, вызываемых
в разные моменты жизненного цикла экземпляра. Архитектурно это
приближает библиотеку к событийно-ориентированным UI-компонентам: любое
изменение состояния транслируется наружу через on(...) и
может быть перехвачено без вмешательства во внутреннюю реализацию.
При переходе к TypeScript или строгой типизации основной сложностью становится отсутствие встроенной типовой модели событий в ранних версиях API. В результате типизация событий опирается на сопоставление строковых идентификаторов событий и структур их payload-данных.
Внутренний набор событий охватывает ключевые операции:
Типичный набор событий:
changeitem_additem_removedropdown_opendropdown_closetypeloadinitializeКаждое событие имеет собственную сигнатуру аргументов, но в JavaScript-версии они не формализованы на уровне типов.
Пример без типизации:
select.on('item_add', function (value, item) {
console.log(value);
console.log(item);
});
При использовании TypeScript возникает фундаментальная проблема: события представлены строками, а их payload — вариативными аргументами функции.
Основные сложности:
create)Это делает невозможным простое описание типа вроде:
on(event: string, handler: Function): void;
Такой подход приводит к потере информации о структуре данных.
Базовый способ типизации — введение отображения событий в виде интерфейса.
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 {}
}
Это позволяет связать строковый ключ события с конкретной сигнатурой обработчика.
item_add: (value: string, item: HTMLElement) => void;
Семантика:
value — значение опцииitem — DOM-узел, созданный для элементаТипизация DOM-элементов особенно важна, поскольку внутри библиотеки
они могут быть либо HTMLElement, либо специализированные
расширения.
change: (value: string) => void;
Особенность заключается в том, что событие может агрегировать
несколько значений при multiple: true. В таком случае
строгая типизация расширяется:
change: (value: string | string[]) => void;
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-типизация:
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;
Такой подход позволяет внутри обработчиков использовать методы экземпляра без потери типизации.
Для более строгой модели используется обобщённый класс:
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 допускает динамическое добавление событий через плагины и пользовательские расширения. Это приводит к нескольким ограничениям:
По этой причине типизация событий часто строится как гибридная модель:
Для сложных интеграций используется композиция EventMap:
type BaseEvents = TomSelectEvents;
type ExtendedEvents = BaseEvents & {
custom_filter: (query: string) => void;
};
Такой подход сохраняет совместимость с базовой библиотекой и расширяет событийную модель без изменения ядра.
В зрелой модели событийная система сводится к трём уровням:
Эта структура отражает баланс между строгой типизацией и гибкостью архитектуры Tom Select, где событийная модель остаётся расширяемой без потери базовой типовой целостности.