Type-safe конфигурация

В современных интерфейсах позиционирование плавающих элементов (tooltip, dropdown, popover) требует гибкой, но при этом строго контролируемой конфигурации. Floating UI изначально проектировалась с учётом интеграции с TypeScript, что позволяет обеспечить типобезопасность на всех уровнях: от базовых параметров позиционирования до кастомных middleware.

Типобезопасная конфигурация решает несколько задач:

  • предотвращение ошибок на этапе компиляции
  • автодополнение и документация прямо в IDE
  • контроль совместимости middleware
  • упрощение рефакторинга

Базовая типизация computePosition

Ключевая функция библиотеки — computePosition. Она принимает два DOM-элемента и объект конфигурации:

computePosition(referenceEl, floatingEl, {
  placement: 'bottom-start',
  middleware: [],
});

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

interface ComputePositionConfig {
  placement?: Placement;
  strategy?: Strategy;
  middleware?: Middleware[];
}

Placement

type Placement =
  | 'top'
  | 'bottom'
  | 'left'
  | 'right'
  | 'top-start'
  | 'top-end'
  | ...

Тип Placement ограничивает допустимые строки. Попытка передать, например, "bottom-middle" приведёт к ошибке компиляции.


Типизация middleware

Middleware — расширяемый механизм, который влияет на позиционирование. Каждый middleware строго типизирован:

interface Middleware {
  name: string;
  options?: any;
  fn: (state: MiddlewareState) => MiddlewareReturn;
}

Однако при использовании готовых middleware типы уже заданы:

import { offset, flip, shift } from '@floating-ui/dom';

computePosition(el1, el2, {
  middleware: [
    offset(10),
    flip(),
    shift({ padding: 8 }),
  ],
});

Пример типобезопасности

offset("10"); // Ошибка: ожидается number
shift({ padding: "8px" }); // Ошибка: padding должен быть number

Генерики в пользовательских middleware

Floating UI позволяет создавать собственные middleware с полной типизацией:

import type { Middleware, MiddlewareState } from '@floating-ui/dom';

function customMiddleware(): Middleware {
  return {
    name: 'custom',
    fn(state: MiddlewareState) {
      return {
        x: state.x + 10,
        y: state.y,
        data: {
          customValue: true,
        },
      };
    },
  };
}

Расширение типа data

Можно явно описывать возвращаемые данные:

type CustomData = {
  customValue: boolean;
};

function customMiddleware(): Middleware<CustomData> {
  return {
    name: 'custom',
    fn(state) {
      return {
        data: {
          customValue: true,
        },
      };
    },
  };
}

Теперь middlewareData.custom будет строго типизирован.


Типизация middlewareData

Результат работы всех middleware доступен в middlewareData:

const { middlewareData } = await computePosition(...);

Тип:

type MiddlewareData = {
  [key: string]: any;
};

Для строгой типизации используется объединение:

type MyMiddlewareData = {
  offset?: { x: number; y: number };
  custom?: { customValue: boolean };
};

Применение:

const data = middlewareData as MyMiddlewareData;

data.custom?.customValue; // тип boolean

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

Floating UI поддерживает два варианта стратегии:

type Strategy = 'absolute' | 'fixed';

Пример:

computePosition(ref, floating, {
  strategy: 'fixed',
});

Попытка передать "sticky" приведёт к ошибке.


Типобезопасность с autoUpdate

Функция autoUpdate отслеживает изменения DOM и автоматически обновляет позицию:

autoUpdate(reference, floating, update, options);

Тип опций:

interface AutoUpdateOptions {
  ancestorScroll?: boolean;
  ancestorResize?: boolean;
  elementResize?: boolean;
  layoutShift?: boolean;
  animationFrame?: boolean;
}

Пример:

autoUpdate(ref, floating, update, {
  ancestorScroll: true,
  animationFrame: false,
});

Ошибочные значения:

ancestorScroll: "yes"; // ошибка

Строгая типизация кастомных конфигураций

При создании обёрток или UI-компонентов полезно описывать собственные типы конфигурации:

type TooltipConfig = {
  placement?: Placement;
  offset?: number;
  hasArrow?: boolean;
};

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

function createTooltip(config: TooltipConfig) {
  return computePosition(ref, floating, {
    placement: config.placement ?? 'top',
    middleware: [
      offset(config.offset ?? 8),
    ],
  });
}

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


Типизация с помощью as const

Для повышения точности типов используется as const:

const config = {
  placement: 'bottom-start',
} as const;

Без as const:

placement: string

С as const:

placement: 'bottom-start'

Это важно для строгого сопоставления с типом Placement.


Ограничение значений через union-типы

Иногда требуется сузить допустимые значения:

type VerticalPlacement = 'top' | 'bottom';

function setPlacement(p: VerticalPlacement) {}

Попытка:

setPlacement('left'); // ошибка

Такой подход полезен при создании API компонентов.


Типизация интеграций (React, Vue)

В React-обёртках Floating UI используется строгая типизация props:

type FloatingProps = {
  placement?: Placement;
  middleware?: Middleware[];
};

Пример:

<Floating placement="top-start" />

IDE автоматически подсказывает допустимые значения.


Типобезопасность и расширяемость

Floating UI строится на принципах:

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

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

  • безопасно комбинировать middleware
  • масштабировать сложные UI-системы
  • снижать количество runtime-ошибок

Частые ошибки без типизации

Без TypeScript возможны проблемы:

placement: 'bottommiddle' // опечатка
offset: '10px'           // неверный тип
middleware: [flip, shift] // забыли вызвать функции

Типобезопасность устраняет такие ошибки ещё до запуска кода.


Практика строгой конфигурации

Оптимальный подход:

  1. Использовать TypeScript
  2. Описывать собственные типы конфигурации
  3. Ограничивать значения через union-типы
  4. Применять as const для литералов
  5. Типизировать middleware и их данные

Глубокая интеграция типов

Floating UI демонстрирует подход, при котором библиотека не просто предоставляет API, а формирует строгую типовую модель:

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

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