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

Типизация конфигурации в Tom Select строится вокруг описания объекта настроек, который передаётся при инициализации инстанса, а также вокруг расширяемых типов опций, элементов и событий. Основная цель типизации — зафиксировать контракт между пользовательским кодом и внутренним поведением компонента, минимизировать ошибки конфигурации и обеспечить автодополнение в редакторах с поддержкой TypeScript.

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

В типизированной среде этот объект описывается интерфейсом:

interface TomSelectSettings {
  valueField?: string;
  labelField?: string;
  searchField?: string | string[];
  disabledField?: string;

  options?: unknown[];
  items?: string[];

  maxItems?: number;
  maxOptions?: number;

  create?: boolean | ((input: string) => boolean);
  createOnBlur?: boolean;

  persist?: boolean;

  preload?: boolean | 'focus';

  load?: (query: string, callback: (options: unknown[]) => void) => void;

  render?: Record<string, (data: any, escape: (v: string) => string) => string>;

  sortField?: string | ((a: any, b: any) => number);

  placeholder?: string;

  onInitialize?: () => void;
  onChange?: (value: string) => void;
}

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

Обобщённые типы опций и элементов

Слабая типизация unknown[] быстро становится ограничением. В реальных проектах структура опции почти всегда известна заранее: идентификатор, метка, дополнительные метаданные.

Типизация улучшается через дженерики:

interface BaseOption {
  id: string;
  title: string;
}

interface TomSelectSettings<TOption = BaseOption> {
  valueField: keyof TOption;
  labelField: keyof TOption;
  searchField?: keyof TOption | (keyof TOption)[];

  options?: TOption[];
  items?: (keyof TOption extends string ? string : never)[];

  load?: (query: string, callback: (options: TOption[]) => void) => void;

  render?: {
    option?: (data: TOption, escape: (v: string) => string) => string;
    item?: (data: TOption, escape: (v: string) => string) => string;
  };
}

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

type UserOption = {
  id: string;
  name: string;
  role: string;
};

const settings: TomSelectSettings<UserOption> = {
  valueField: 'id',
  labelField: 'name',
  searchField: ['name', 'role'],
};

Компилятор теперь контролирует соответствие ключей valueField, labelField и searchField реальной структуре объекта.

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

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

interface TomSelectEvents<TOption> {
  onInitialize?: () => void;
  onChange?: (value: string | string[]) => void;
  onItemAdd?: (value: string, item: TOption) => void;
  onItemRemove?: (value: string) => void;
  onOptionAdd?: (option: TOption) => void;
}

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

Типизация рендеринга

Рендер-функции являются одним из наиболее гибких, но и наиболее опасных мест конфигурации. Они напрямую формируют HTML, что требует строгого контроля типов входных данных.

interface TomSelectRender<TOption> {
  option?: (data: TOption, escape: (value: string) => string) => string;
  item?: (data: TOption, escape: (value: string) => string) => string;
  optgroup_header?: (data: any, escape: (value: string) => string) => string;
  no_results?: (data: { input: string }, escape: (value: string) => string) => string;
}

Здесь важным аспектом является функция escape, которая типизируется отдельно и используется для предотвращения XSS при генерации HTML.

Расширение конфигурации через module augmentation

Одной из ключевых возможностей TypeScript является расширение существующих интерфейсов без изменения исходного кода библиотеки.

declare module 'tom-select' {
  interface TomSelectSettings<TOption> {
    apiEndpoint?: string;
    debounceTime?: number;
  }
}

Это позволяет адаптировать конфигурацию под специфические требования проекта, например добавлять параметры для интеграции с REST API или GraphQL без форка библиотеки.

Типизация загрузки данных

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

interface LoadCallback<TOption> {
  (options: TOption[]): void;
}

interface TomSelectSettings<TOption> {
  load?: (
    query: string,
    callback: LoadCallback<TOption>
  ) => void | Promise<void>;
}

При использовании Promise-ориентированного подхода добавляется дополнительный уровень контроля:

load?: (query: string) => Promise<TOption[]>;

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

Типизация состояния и значений

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

interface TomSelectState<TValue = string> {
  items: TValue[];
  options: Record<string, unknown>[];
  activeItems: TValue[];
}

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

interface TomSelectSettings<TOption, TValue extends keyof TOption = keyof TOption> {
  valueField: TValue;
  items?: TOption[TValue][];
}

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

Типизация опций группировки

Группы (optgroups) требуют отдельного описания структуры, так как они отделены от обычных опций.

interface OptGroup {
  value: string;
  label: string;
  options: unknown[];
}

interface TomSelectSettings<TOption> {
  optgroups?: OptGroup[];
  optgroupField?: keyof TOption;
}

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

Конфигурация плагинов и расширений

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

interface PluginSettings {
  [pluginName: string]: unknown;
}

interface TomSelectSettings<TOption> {
  plugins?: string[] | PluginSettings;
}

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

interface DragDropPluginSettings {
  enableSwap?: boolean;
}

declare module 'tom-select' {
  interface TomSelectSettings<TOption> {
    dragDrop?: DragDropPluginSettings;
  }
}

Строгая конфигурация и вывод типов

При использовании as const и infer-типизации можно автоматически извлекать типы из конфигурации:

const config = {
  valueField: 'id',
  labelField: 'name',
  maxItems: 5
} as const;

type ConfigType = typeof config;

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

Итоговая структура типовой модели

В зрелой типизации конфигурация превращается в композицию нескольких слоёв:

  • базовые настройки поведения;
  • типизированные опции и значения;
  • события жизненного цикла;
  • рендер-функции;
  • асинхронные загрузчики;
  • плагины и расширения;
  • доменные расширения через module augmentation.

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