Типизация плагинов

Система плагинов в Tom Sel ect строится вокруг расширяемой архитектуры, в которой каждый плагин представляет собой изолированный модуль, подключаемый к экземпляру селекта и способный модифицировать его поведение, DOM-структуру, события и внутреннее состояние. При использовании TypeScript ключевую роль играет корректная типизация точек расширения, поскольку плагины фактически становятся частью жизненного цикла компонента.

Базовая модель плагина в типах описывается как функция, принимающая экземпляр Tom Select и возвращающая void либо набор хуков:

export type TomSelectPlugin = (instance: TomSelect) => void;

Однако в реальной типизации этого недостаточно, поскольку плагины могут:

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

Поэтому полноценная типизация строится вокруг расширения интерфейса экземпляра и декларации модуля.


Базовый контракт плагина

Каждый плагин в TypeScript контексте рассматривается как функция-инициализатор, которая получает ссылку на экземпляр и работает с ним через публичный интерфейс:

interface TomSelect {
  on(event: string, handler: (...args: any[]) => void): void;
  off(event: string, handler: (...args: any[]) => void): void;
  addItem(value: string): void;
  removeItem(value: string): void;
  refreshOptions(triggerDropdown: boolean): void;
}

Плагин использует этот интерфейс как точку входа, но часто расширяет его:

function myPlugin(ts: TomSelect) {
  ts.on('change', () => {
    // логика плагина
  });
}

Расширение экземпляра через декларацию интерфейса

Ключевая особенность типизации плагинов заключается в возможности расширения самого экземпляра Tom Select. Это делается через declaration merging.

declare module 'tom-select' {
  interface TomSelect {
    highlightActive: () => void;
  }
}

После этого плагин может безопасно добавлять методы:

function highlightPlugin(ts: TomSelect) {
  ts.highlightActive = function () {
    const active = ts.dropdown.querySelector('.active');
    if (active) active.classList.add('highlight');
  };
}

Такой подход обеспечивает:

  • отсутствие необходимости кастов типа any
  • сохранение автодополнения
  • контроль структуры расширений

Типизация плагинов через generics

Более строгий уровень типизации достигается через дженерики, позволяющие описывать расширение экземпляра:

type Plugin<T = {}> = (instance: TomSelect & T) => void;

Использование:

type HighlightExtension = {
  highlightActive: () => void;
};

const highlightPlugin: Plugin<HighlightExtension> = (ts) => {
  ts.highlightActive = () => {
    ts.refreshOptions(true);
  };
};

Такой подход позволяет явно фиксировать контракт расширений, что особенно важно в больших системах с множеством плагинов.


Типизация событий внутри плагинов

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

Базовый вариант:

type TomSelectEvent =
  | 'initialize'
  | 'change'
  | 'item_add'
  | 'item_remove'
  | 'dropdown_open'
  | 'dropdown_close';

Расширенный вариант с маппингом:

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

Тогда плагин может использовать строго типизированную подписку:

function typedPlugin(ts: TomSelect) {
  ts.on('item_add', (value: string, item: HTMLElement) => {
    item.classList.add('plugin-added');
  });
}

Интеграция плагинов в конфигурацию

Tom Select позволяет подключать плагины через конфигурационный объект. В типах это выражается через поле plugins, которое может быть строковым списком или словарём функций:

interface TomSelectOptions {
  plugins?: string[] | Record<string, TomSelectPlugin>;
}

Расширенный вариант с типизацией:

type PluginMap = Record<string, TomSelectPlugin>;

interface TomSelectOptions {
  plugins?: (keyof PluginMap)[] | PluginMap;
}

Это позволяет:

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

Типизация встроенных плагинов

Встроенные плагины Tom Select также требуют описания контрактов, поскольку они расширяют поведение компонента. Например, плагин remove_button добавляет кнопки удаления элементов.

Типизация такого расширения:

interface RemoveButtonPluginOptions {
  title?: string;
  label?: string;
}

И соответствующее расширение экземпляра:

declare module 'tom-select' {
  interface TomSelect {
    removeButton: {
      createButton: (item: HTMLElement) => HTMLElement;
    };
  }
}

Плагины с состоянием

Некоторые плагины сохраняют внутреннее состояние, которое должно быть типизировано отдельно от экземпляра:

interface PluginState {
  enabled: boolean;
  cache: Map<string, HTMLElement>;
}

Использование внутри плагина:

function statefulPlugin(ts: TomSelect) {
  const state: PluginState = {
    enabled: true,
    cache: new Map()
  };

  ts.on('item_add', (value, item) => {
    state.cache.set(value, item);
  });
}

Такой подход предотвращает утечки типов в глобальный контекст и изолирует данные плагина.


Типизация конфликтов плагинов

При комбинировании нескольких плагинов возникает риск конфликтов расширений экземпляра. TypeScript решает это через объединение интерфейсов, но логически требуется учитывать пересечения.

interface TomSelect {
  highlightActive(): void;
  highlightActive(): void; // конфликт сигнатур невозможен
}

Конфликт возникает при несовместимых расширениях, поэтому используется стратегия пространств имён:

declare module 'tom-select' {
  interface TomSelect {
    highlightPlugin: {
      highlightActive: () => void;
    };
  }
}

Это позволяет изолировать расширения и избежать коллизий.


Фабричная типизация плагинов

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

type PluginFactory<TOptions = {}> = (options: TOptions) => TomSelectPlugin;

Пример:

interface HighlightOptions {
  className: string;
}

const createHighlightPlugin: PluginFactory<HighlightOptions> =
  (options) => (ts) => {
    ts.on('item_add', (_, item) => {
      item.classList.add(options.className);
    });
  };

Этот подход позволяет:

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

Типизация удаления плагина

Некоторые реализации требуют возможности отключения плагина. Для этого вводится контракт cleanup-функции:

type PluginDestroy = () => void;

type TomSelectPlugin = (instance: TomSelect) => PluginDestroy | void;

Пример:

const plugin: TomSelectPlugin = (ts) => {
  const handler = () => {
    ts.refreshOptions(false);
  };

  ts.on('change', handler);

  return () => {
    ts.off('change', handler);
  };
};

Такой механизм обеспечивает:

  • корректное освобождение ресурсов
  • отсутствие утечек событий
  • предсказуемое поведение при удалении плагинов

Модульная типизация плагинов

При масштабных проектах плагины оформляются как отдельные модули с собственными декларациями:

// highlight-plugin.d.ts
declare module 'tom-select/plugins/highlight' {
  const plugin: TomSelectPlugin;
  export default plugin;
}

И использование:

import highlight fr om 'tom-select/plugins/highlight';

Это позволяет:

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

Итоговая структура типового плагина

Типичный хорошо типизированный плагин в экосистеме Tom Select включает:

  • функцию-инициализатор с типом TomSelectPlugin
  • расширение интерфейса через declaration merging
  • строго типизированные события
  • изолированное состояние
  • опциональную фабрику конфигурации
  • cleanup-функцию при необходимости

Такая структура обеспечивает предсказуемую интеграцию в систему, где каждый плагин становится формально описанным расширением базового компонента, а не неявной модификацией поведения.