Интерфейсы и дженерики

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

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

  • локали и их идентификаторы;
  • CLDR-данные (числа, даты, единицы измерения);
  • форматтеры и их конфигурации;
  • результирующие строки и промежуточные представления.

Базовые интерфейсы локализации

В основе типизации лежит описание локали как строго структурированного объекта или строкового идентификатора.

interface Locale {
  name: string;
  language: string;
  script?: string;
  region?: string;
}

Такой интерфейс позволяет отделить синтаксическое представление локали ("en-US") от её разложенной формы. В типизированной модели это упрощает работу с частичными локалями и fallback-механизмами.

Дополнительно вводится обобщённый тип для поддержки различных представлений локали:

type LocaleInput = string | Locale;

Это позволяет API оставаться совместимым с нативным форматом Globalize и одновременно поддерживать расширенные структуры.

Интерфейсы CLDR-данных

CLDR (Common Locale Data Repository) представляет собой фундаментальную зависимость. Типизация CLDR-структур критична, поскольку данные глубоко вложенные и часто неоднородные.

interface CldrNumberSymbols {
  decimal: string;
  group: string;
  plusSign: string;
  minusSign: string;
}
interface CldrNumberFormats {
  decimal: string;
  scientific: string;
  percent: string;
  currency: string;
}

Объединение этих интерфейсов формирует базовую модель числовой локализации:

interface CldrNumbers {
  symbols: CldrNumberSymbols;
  formats: CldrNumberFormats;
}

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

Дженерики в форматерах

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

interface Formatter<TInput, TOutput> {
  format(value: TInput): TOutput;
}

Этот контракт описывает универсальный форматтер, где:

  • TInput — тип входного значения (число, дата, единица измерения);
  • TOutput — тип результата (обычно строка, но не обязательно).

Числовой форматтер

type NumberFormatter = Formatter<number, string>;

Числовой форматтер в рамках Globalize работает с CLDR-данными и локалью, но интерфейс скрывает эти детали, оставляя только вход и выход.

Форматтер дат

type DateFormatter = Formatter<Date, string>;

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

Параметризованные конфигурации форматирования

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

interface FormatOptions<T> {
  locale: LocaleInput;
  raw?: boolean;
  pattern?: string;
  transform?: (value: T) => T;
}

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

Пример специализации:

type NumberFormatOptions = FormatOptions<number>;
type DateFormatOptions = FormatOptions<Date>;

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

Интерфейсы парсеров

В обратную сторону от форматирования работает парсинг строк в типизированные значения.

interface Parser<TInput, TOutput> {
  parse(value: TInput): TOutput;
}

В контексте Globalize это критично для обработки локализованных чисел и дат.

Пример числового парсера:

type NumberParser = Parser<string, number>;

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

Обобщённые фабрики форматтеров

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

interface FormatterFactory<TInput, TOutput, TOptions> {
  create(options: TOptions): Formatter<TInput, TOutput>;
}

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

Пример для чисел:

interface NumberFormatFactory
  extends FormatterFactory<number, string, NumberFormatOptions> {}

Это обеспечивает строгую типизацию цепочки:

конфигурация → фабрика → форматтер → результат.

Условные типы для локалей и fallback

В сложных системах интернационализации используются условные типы для управления fallback-локалями.

type FallbackLocale<T extends string> =
  T extends `${infer Lang}-${infer Region}`
    ? Lang
    : T;

Такой механизм позволяет автоматически извлекать базовый язык из составной локали.

Применение в API:

type NormalizedLocale<T extends LocaleInput> =
  T extends string ? FallbackLocale<T> : T;

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

Обобщённые коллекции локализованных данных

При работе с несколькими локалями одновременно используется параметризация коллекций.

interface LocaleMap<T> {
  [locale: string]: T;
}

Пример:

type DateFormatMap = LocaleMap<string>;

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

Связь интерфейсов с runtime-архитектурой

Хотя интерфейсы и дженерики существуют только на этапе компиляции, их структура напрямую отражает архитектуру Globalize:

  • форматтеры как чистые функции;
  • парсеры как обратные трансформеры;
  • CLDR как строго типизированное хранилище данных;
  • локаль как центральный идентификатор контекста.

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

Композиция дженериков в цепочках обработки

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

type Pipeline<TInput, TMiddle, TOutput> = {
  first: Formatter<TInput, TMiddle>;
  second: Formatter<TMiddle, TOutput>;
};

Такая модель позволяет описывать цепочки трансформации данных:

входное значение → промежуточное представление → финальный результат

без потери типовой информации на каждом этапе.

Расширяемость через интерфейсы

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

interface ExtendedNumberFormatter extends NumberFormatter {
  formatToParts(value: number): Array<{ type: string; value: string }>;
}

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

В рамках Globalize это особенно важно, поскольку экосистема CLDR и локалей постоянно расширяется, а API должен оставаться стабильным.