Строгая типизация данных

При интеграции Tom Select в современные JavaScript/TypeScript-приложения ключевым аспектом становится формализация структуры данных, с которыми работает компонент. Библиотека изначально ориентирована на гибкость, однако в условиях строгой типизации требуется явно фиксировать контракты: что считается опцией, как выглядит выбранное значение, как описываются пользовательские данные и каким образом они трансформируются в процессе работы селектора.

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


Базовые типы данных: Option и Value

Ядро модели данных Tom Select строится вокруг двух сущностей:

  • Option — элемент списка выбора
  • Value — значение, которое хранится как выбранное

В строгой типизации важно понимать, что эти сущности не всегда совпадают.

Типичная форма опции:

type TomOption = {
  value: string | number;
  text: string;
  disabled?: boolean;
  $order?: number;
};

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


Обобщённая модель через generics

При работе с TypeScript основной подход заключается в параметризации Tom Select типом опции:

interface BaseOption {
  value: string | number;
  text: string;
}

type ExtendedOption = BaseOption & {
  category?: string;
  meta?: {
    priority: number;
  };
};

Далее Tom Select может быть типизирован как:

const select = new TomSelect<ExtendedOption>("#select", {
  valueField: "value",
  labelField: "text"
});

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


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

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

При строгой типизации важно разделить конфигурацию на логические блоки:

interface TomSelectConfig<T> {
  valueField: keyof T;
  labelField: keyof T;
  searchField?: (keyof T)[];
  disabledField?: keyof T;

  create?: boolean;
  maxItems?: number;

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

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


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

Методы Tom Select также требуют строгого описания входных и выходных данных.

addOption

addOption(option: T): void;

Метод принимает строго типизированный объект, соответствующий generic-параметру. Это исключает возможность добавления «поломанных» структур.


addItem

addItem(value: T["value"], silent?: boolean): void;

Здесь важен второй уровень типизации: значение должно соответствовать типу поля value внутри T. Это предотвращает ситуацию, когда в селектор передаётся строка вместо числа или наоборот.


getItem

getItem(value: T["value"]): HTMLElement | null;

Возвращаемый тип DOM-элемента остаётся фиксированным, однако входное значение строго связано с типом данных опции.


Нормализация данных

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

  • REST API
  • локальные массивы
  • динамические вычисления
  • серверные фильтры

Для строгой типизации вводится слой нормализации:

type RawOption = unknown;

function normalizeOption<T>(raw: RawOption): T {
  const obj = raw as T;

  if (!obj || typeof obj !== "object") {
    throw new Error("Invalid option format");
  }

  return obj;
}

Более строгий вариант предполагает runtime-валидацию:

function isOption(obj: any): obj is TomOption {
  return obj && typeof obj.value !== "undefined" && typeof obj.text === "string";
}

Такой подход позволяет объединить TypeScript-типизацию и runtime-защиту.


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

Tom Select активно использует событийную модель: изменения выбора, добавление опций, удаление элементов.

Строгая типизация событий требует описания payload каждого события:

interface TomSelectEvents<T> {
  "change": (value: T["value"][]) => void;
  "item_add": (value: T["value"], item: HTMLElement) => void;
  "item_remove": (value: T["value"]) => void;
  "option_add": (option: T) => void;
}

При использовании дженериков события становятся строго привязанными к модели данных.


Типизация поиска и фильтрации

Поисковый механизм Tom Select зависит от полей, указанных в searchField. При строгой типизации важно ограничить возможные ключи:

type SearchableFields<T> = Extract<keyof T, string>;

interface SearchConfig<T> {
  searchField: SearchableFields<T>[];
  score?: (item: T, query: string) => number;
}

Это исключает передачу несуществующих или нестроковых полей в систему поиска.


Типизация создания новых элементов

Функция create в Tom Select позволяет добавлять новые элементы, которых нет в исходном наборе данных.

interface CreateConfig<T> {
  create: boolean | ((input: string) => T);
}

При строгой типизации важно обеспечить соответствие результата функции структуре T. Это предотвращает попадание «частичных» объектов в список опций.


Расширение типов через композицию

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

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

type WithAvatar = {
  avatarUrl: string;
};

type FullOption = UserOption & WithAvatar;

Tom Select при этом работает с итоговым типом:

const select = new TomSelect<FullOption>("#users", {
  valueField: "id",
  labelField: "name"
});

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


Ограничение мутабельности данных

Строгая типизация в контексте Tom Select часто дополняется ограничением изменяемости:

type ReadonlyOption<T> = Readonly<T>;

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


Типизация рендер-функций

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

type RenderFunction<T> = (
  data: T,
  escape: (value: string) => string
) => string;

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


Типизация плагинов и расширений

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

interface TomSelectPlugin<T> {
  initialize(select: TomSelect<T>): void;
  destroy?(): void;
}

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


Контроль приведения типов

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

const options = response.data as TomOption[];

Однако при строгом подходе предпочтение отдаётся проверке:

const options = response.data.filter(isOption);

Это снижает риск попадания некорректных данных в состояние селектора.


Типизация состояния компонента

Внутреннее состояние Tom Select может быть формализовано следующим образом:

interface SelectState<T> {
  items: T["value"][];
  options: T[];
  activeIndex: number | null;
}

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


Взаимодействие с формами и внешними библиотеками

При интеграции с form-менеджерами (например, react-hook-form или аналогами в vanilla-подходах) типизация должна учитывать сериализацию значения:

type FormValue<T> = T["value"] | T["value"][];

Это позволяет унифицировать работу как с single-select, так и multi-select режимами без потери строгой типизации.