Расширение базовых типов

Tom Select в стандартной конфигурации опирается на простую модель данных, где ключевыми сущностями выступают Option, Item и конфигурационный объект Settings. При переходе к масштабируемым интерфейсам этой модели становится недостаточно, и возникает необходимость расширения типов без нарушения внутренней логики библиотеки.

В базовом виде Option представляет объект, содержащий как минимум идентификатор и отображаемое значение:

  • value: string | number
  • text: string

Item обычно является упрощённой проекцией Option, используемой внутри выбранных значений. Однако на практике эти структуры редко остаются примитивными. Почти всегда требуется добавление бизнес-полей: статус, метки, вложенные данные, связи с внешними сущностями.

Расширение типов в TypeScript строится вокруг принципа структурного расширения, а не наследования. Это позволяет адаптировать модель данных без изменения исходного кода библиотеки.


Расширение базовой модели Option и Item

Наиболее распространённый сценарий — добавление пользовательских полей в Option. Например, если список содержит пользователей:

type UserOption = {
  value: string;
  text: string;
  role: string;
  department: string;
  isActive: boolean;
};

В таком случае необходимо синхронизировать типы Option и Item, поскольку Tom Select может преобразовывать данные между состояниями.

Типизация обычно строится через дженерики (если обёртка поддерживает их):

interface MyOption {
  value: string;
  text: string;
  meta: {
    role: string;
    department: string;
  };
}

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


Обогащение структуры данных

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

  • организация → отдел → сотрудник
  • категория → подкатегория → элемент

Типизация таких структур требует аккуратного подхода:

type DepartmentOption = {
  value: string;
  text: string;
  organization: {
    id: string;
    name: string;
  };
};

В подобных случаях text может перестать быть достаточным для отображения, и его формирование переносится в слой render.


Расширение конфигурации Settings

Конфигурационный объект Settings в Tom Select является одним из ключевых мест расширения поведения. Он включает:

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

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

Пример расширенного интерфейса:

interface ExtendedSettings {
  maxItems: number;
  searchField: string[];
  debounceTime: number;
  allowCreate: boolean;
  normalizeInput: (input: string) => string;
}

Особое значение имеет типизация функций, поскольку именно они определяют точки расширения логики. Например:

  • score: (search: string, option: Option) => number
  • filter: (option: Option) => boolean

Эти функции фактически превращают конфигурацию в поведенческий слой.


Типизация render-функций

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

Типичная структура:

interface RenderTemplates<TOption> {
  option: (data: TOption, escape: (str: string) => string) => string;
  item: (data: TOption, escape: (str: string) => string) => string;
  option_create?: (data: { input: string }) => string;
}

При расширении типов Option необходимо синхронизировать их с render, иначе возникает рассинхронизация между данными и отображением.

Ключевой момент: render не ограничивает структуру данных, но ожидает её стабильность. Это означает, что изменение формы Option должно сопровождаться изменением всех render-функций.


Расширение событийной модели

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

Примеры событий:

  • change
  • item_add
  • item_remove
  • option_add
  • dropdown_open

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

type TomSelectEvents<T> = {
  change: (value: T[]) => void;
  item_add: (value: T) => void;
  item_remove: (value: T) => void;
};

Расширение событийной модели особенно важно при интеграции с архитектурами:

  • Redux-подобные сторы
  • Event bus системы
  • реактивные фреймворки

События становятся мостом между внутренним состоянием компонента и внешней логикой приложения.


Плагинная система и расширение типов

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

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

Пример типизации плагина:

interface PluginContext<T> {
  select: TomSelect<T>;
  settings: ExtendedSettings;
}

Плагин может расширять интерфейс экземпляра:

interface TomSelect<T> {
  customMethod(): void;
}

Это достигается через module augmentation, что позволяет безопасно добавлять методы без конфликтов типов.


Module augmentation и декларативное расширение

В TypeScript основной механизм расширения библиотек — декларативное слияние модулей.

declare module "tom-select" {
  interface TomSelect<T> {
    refreshData(): void;
  }
}

Такой подход позволяет:

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

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


Переопределение прототипов и runtime-расширение

Несмотря на то, что типизация работает на уровне компиляции, сама библиотека позволяет расширять поведение через прототип.

Пример:

TomSelect.prototype.refreshData = function () {
  this.clearOptions();
  this.load(callback);
};

При этом TypeScript-типизация должна быть синхронизирована вручную через augmentation.

Такой подход создаёт расхождение между runtime и compile-time моделью, которое необходимо контролировать.


Сложные доменные модели

При работе с реальными системами данные редко бывают плоскими. Расширенные типы часто включают:

  • иерархии
  • ссылки на внешние сущности
  • вычисляемые поля
  • локализованные значения

Пример:

type ProductOption = {
  value: string;
  text: string;
  price: number;
  currency: string;
  stock: {
    available: number;
    reserved: number;
  };
  category: {
    id: string;
    name: string;
  };
};

В таких моделях Tom Select выступает только как слой выбора, а вся сложность переносится в типы данных.


Типизация асинхронных источников данных

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

  • загрузка
  • ошибка
  • частичный результат

Тип функции загрузки:

type LoadFunction<T> = (query: string, callback: (items: T[]) => void) => void;

Расширенные варианты включают Promise:

type AsyncLoad<T> = (query: string) => Promise<T[]>;

Это позволяет интегрировать Tom Select с современными API-архитектурами.


Совместимость и деградация типов

При расширении базовых типов важно учитывать обратную совместимость. Любое расширение должно:

  • сохранять обязательные поля value и text
  • не ломать сигнатуры render-функций
  • поддерживать fallback-структуры
  • учитывать поведение legacy-кода

Деградация типов часто проявляется при смешивании разных источников данных, когда часть опций имеет расширенную структуру, а часть — базовую. В таких случаях применяется объединение:

type MixedOption = BaseOption | ExtendedOption;

или нормализация через адаптер:

function normalize(option: any): BaseOption {
  return {
    value: option.value,
    text: option.text ?? String(option.value)
  };
}

Расширение поведенческих контрактов

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

  • как данные поступают в компонент
  • как они трансформируются
  • как возвращаются наружу

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