Механизм инференса типов в контексте конфигурации Popper.js становится особенно актуальным при использовании TypeScript. Он позволяет автоматически выводить типы на основе переданных параметров, минимизируя количество явных аннотаций и снижая вероятность ошибок.
Конфигурация Popper состоит из нескольких ключевых полей:
placementmodifiersstrategyonFirstUpdateТипизация этих полей задаётся через обобщённые типы
(generics) и условные конструкции TypeScript.
Пример базовой конфигурации:
import { createPopper } from '@popperjs/core';
const popper = createPopper(referenceElement, popperElement, {
placement: 'top',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
});
placementПоле placement принимает строго ограниченный набор
строковых значений:
type Placement =
| 'auto'
| 'top'
| 'bottom'
| 'right'
| 'left'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end';
При передаче значения напрямую TypeScript автоматически сужает тип:
const config = {
placement: 'top',
};
Тип config.placement будет 'top', а не
string, благодаря литеральному типу.
Однако при вынесении значения в переменную без as const
происходит расширение:
const placement = 'top';
const config = {
placement,
};
Теперь placement имеет тип string, и
теряется точность. Для сохранения инференса используется:
const placement = 'top' as const;
или:
const config = {
placement: 'top',
} as const;
Массив modifiers — наиболее сложная часть конфигурации с
точки зрения типизации. Каждый модификатор имеет структуру:
type Modifier<Name, Options> = {
name: Name;
enabled?: boolean;
phase?: ModifierPhases;
fn?: (state: State) => void;
options?: Options;
};
TypeScript способен выводить тип options на основе имени
модификатора, если используется заранее описанный набор:
import { Modifier } from '@popperjs/core';
const offsetModifier: Modifier<'offset', { offset: [number, number] }> = {
name: 'offset',
options: {
offset: [0, 10],
},
};
Однако при использовании встроенных модификаторов инференс происходит автоматически:
const config = {
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
};
TypeScript понимает, что offset — это массив из двух
чисел, благодаря встроенным декларациям типов.
При создании пользовательских модификаторов важно сохранить корректный инференс:
type CustomOptions = {
customValue: number;
};
const customModifier: Modifier<'custom', CustomOptions> = {
name: 'custom',
enabled: true,
phase: 'main',
fn({ state, options }) {
console.log(options.customValue);
},
};
Если не указать тип CustomOptions, поле
options будет иметь тип any, что разрушает
преимущества строгой типизации.
createPopperФункция createPopper использует обобщения для связывания
типов элементов и конфигурации:
function createPopper<
TReferenceElement extends Element | VirtualElement,
TPopperElement extends HTMLElement
>(
reference: TReferenceElement,
popper: TPopperElement,
options?: Partial<Options>
): Instance;
Инференс происходит автоматически:
const instance = createPopper(buttonElement, tooltipElement);
TypeScript выводит:
TReferenceElement из buttonElementTPopperElement из tooltipElementPartial<Options>)Конфигурация передаётся как Partial<Options>, что
позволяет указывать только нужные поля:
const config = {
placement: 'bottom',
};
TypeScript не требует полного описания всех свойств, но при этом сохраняет строгую проверку существующих.
as constКлючевой инструмент для точного вывода типов —
as const.
Без него:
const modifiers = [
{
name: 'offset',
options: { offset: [0, 8] },
},
];
Тип name будет string, а не
'offset'.
С as const:
const modifiers = [
{
name: 'offset',
options: { offset: [0, 8] },
},
] as const;
Теперь:
name имеет тип 'offset'offset — [0, 8], а не
number[]Это критично для корректной работы типовой системы Popper.
Некоторые типы в Popper зависят друг от друга. Например, тип
options зависит от name модификатора.
Внутри библиотеки используются условные типы:
type ModifierOptionsByName<T> =
T extends 'offset' ? OffsetOptions :
T extends 'flip' ? FlipOptions :
unknown;
Это позволяет TypeScript автоматически подставлять нужную структуру
options.
Несмотря на мощные возможности, инференс имеет ограничения:
Потеря литеральных типов при мутациях
let config = { placement: 'top' };
config.placement = 'bottom';
Тип становится string.
Сложные вычисляемые значения
const placement = getPlacement();
Тип не может быть выведен точно.
Массивы без as const
const arr = ['top', 'bottom'];
Тип: string[], а не
('top' | 'bottom')[].
Рекомендуемый подход:
import type { Options } from '@popperjs/core';
const config: Partial<Options> = {
placement: 'right',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 12],
},
},
],
};
Либо более строгий вариант:
const config = {
placement: 'right',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 12],
},
},
],
} as const;
Инференс типов напрямую связан с архитектурой модификаторов:
optionsЭто делает Popper.js примером библиотеки с глубокой и продуманной типовой системой, где конфигурация становится не просто объектом, а строго проверяемой структурой.