Создание типобезопасных оберток

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

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


Базовая типизация Popper.js

Popper.js (v2) уже поставляется с типами (TypeScript), но они достаточно обобщённые:

import { createPopper, Instance, Placement, Modifier } from '@popperjs/core';

const instance: Instance = createPopper(reference, popper, {
  placement: 'top',
});

Основные проблемы:

  • placement — строковый union, но без ограничений контекста
  • modifiers — массив слабо типизированных объектов
  • отсутствует контроль связей между параметрами

Создание строгого типа конфигурации

Первый шаг — ограничение допустимых значений и структур:

type StrictPlacement =
  | 'top'
  | 'bottom'
  | 'left'
  | 'right';

interface StrictPopperOptions {
  placement: StrictPlacement;
  offset?: [number, number];
  strategy?: 'absolute' | 'fixed';
}

Теперь конфигурация становится предсказуемой:

const options: StrictPopperOptions = {
  placement: 'top',
  offset: [0, 8],
};

Типобезопасная обёртка над createPopper

Создание обёртки позволяет контролировать входные параметры:

function createStrictPopper(
  reference: HTMLElement,
  popper: HTMLElement,
  options: StrictPopperOptions
): Instance {
  return createPopper(reference, popper, {
    placement: options.placement,
    strategy: options.strategy,
    modifiers: [
      {
        name: 'offset',
        options: {
          offset: options.offset ?? [0, 0],
        },
      },
    ],
  });
}

Преимущества:

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

Типизация модификаторов

Модификаторы — ключевой механизм Popper.js. Без типизации они становятся источником ошибок.

Создание строгого интерфейса:

interface OffsetModifier {
  name: 'offset';
  options: {
    offset: [number, number];
  };
}

interface FlipModifier {
  name: 'flip';
  options?: {
    fallbackPlacements?: StrictPlacement[];
  };
}

type StrictModifier = OffsetModifier | FlipModifier;

Теперь массив модификаторов строго контролируется:

const modifiers: StrictModifier[] = [
  {
    name: 'offset',
    options: { offset: [0, 10] },
  },
  {
    name: 'flip',
    options: { fallbackPlacements: ['top', 'bottom'] },
  },
];

Генерики для расширяемости

Для масштабируемых систем важно предусмотреть расширение:

type BaseOptions<TModifiers> = {
  placement: StrictPlacement;
  modifiers?: TModifiers[];
};

function createTypedPopper<TModifiers>(
  reference: HTMLElement,
  popper: HTMLElement,
  options: BaseOptions<TModifiers>
): Instance {
  return createPopper(reference, popper, {
    placement: options.placement,
    modifiers: options.modifiers as any,
  });
}

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

const instance = createTypedPopper<StrictModifier>(
  ref,
  pop,
  {
    placement: 'bottom',
    modifiers,
  }
);

Ограничение комбинаций параметров

Типобезопасность позволяет запрещать некорректные комбинации:

type FixedStrategyOptions = {
  strategy: 'fixed';
  placement: 'top' | 'bottom';
};

type AbsoluteStrategyOptions = {
  strategy: 'absolute';
  placement: StrictPlacement;
};

type SmartOptions = FixedStrategyOptions | AbsoluteStrategyOptions;

Теперь:

const options: SmartOptions = {
  strategy: 'fixed',
  placement: 'left', // ошибка
};

Типизация DOM-элементов

Popper принимает Element | VirtualElement. Часто требуется уточнение:

type ReferenceElement = HTMLElement;
type PopperElement = HTMLElement;

function isHTMLElement(el: Element): el is HTMLElement {
  return el instanceof HTMLElement;
}

Применение в обёртке:

function safeCreatePopper(
  reference: Element,
  popper: Element,
  options: StrictPopperOptions
): Instance {
  if (!isHTMLElement(reference) || !isHTMLElement(popper)) {
    throw new Error('Invalid elements');
  }

  return createStrictPopper(reference, popper, options);
}

Инкапсуляция поведения

Обёртка может включать дополнительную бизнес-логику:

class PopperController {
  private instance: Instance | null = null;

  constructor(
    private reference: HTMLElement,
    private popper: HTMLElement
  ) {}

  init(options: StrictPopperOptions) {
    this.instance = createStrictPopper(
      this.reference,
      this.popper,
      options
    );
  }

  update() {
    this.instance?.update();
  }

  destroy() {
    this.instance?.destroy();
    this.instance = null;
  }
}

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

Popper имеет внутреннее состояние (state), которое можно типизировать:

import { State } from '@popperjs/core';

function logPlacement(state: State) {
  const placement: Placement = state.placement;
  console.log(placement);
}

Расширение:

interface ExtendedState extends State {
  customData?: {
    initializedAt: number;
  };
}

Типобезопасные фабрики

Фабричный подход упрощает создание стандартных конфигураций:

function createTooltipPopper(
  reference: HTMLElement,
  tooltip: HTMLElement
): Instance {
  return createStrictPopper(reference, tooltip, {
    placement: 'top',
    offset: [0, 8],
  });
}

Интеграция с UI-фреймворками

В React:

type UsePopperOptions = StrictPopperOptions;

function useTypedPopper(
  reference: HTMLElement | null,
  popper: HTMLElement | null,
  options: UsePopperOptions
) {
  useEffect(() => {
    if (!reference || !popper) return;

    const instance = createStrictPopper(reference, popper, options);

    return () => instance.destroy();
  }, [reference, popper, options]);
}

Защита от runtime-ошибок

Типобезопасные обёртки:

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

Баланс строгости и гибкости

Слишком строгая типизация может ограничивать возможности Popper.js. Оптимальный подход:

  • строгие типы для часто используемых сценариев
  • возможность расширения через generics
  • fallback к оригинальному API при необходимости

Архитектурные преимущества

Типобезопасные обёртки:

  • упрощают рефакторинг
  • повышают читаемость кода
  • создают единый стандарт использования Popper.js
  • уменьшают когнитивную нагрузку

Практический шаблон

interface AppPopperOptions {
  placement: StrictPlacement;
  offset?: [number, number];
}

function createAppPopper(
  ref: HTMLElement,
  pop: HTMLElement,
  options: AppPopperOptions
) {
  return createPopper(ref, pop, {
    placement: options.placement,
    modifiers: [
      {
        name: 'offset',
        options: {
          offset: options.offset ?? [0, 8],
        },
      },
      {
        name: 'flip',
      },
    ],
  });
}

Такой подход формирует устойчивый, масштабируемый и безопасный слой взаимодействия с Popper.js.