Type guards

В экосистеме JavaScript и TypeScript термин type guards обозначает механизмы уточнения типа значения в рантайме. При работе с библиотекой Globalize это особенно важно из-за того, что форматирование и парсинг локализованных данных часто принимают значения неопределённого или широкого типа: string | number | Date | null | unknown.

Globalize не накладывает строгих ограничений на входные данные на уровне исполнения, поэтому контроль типов переносится в прикладной код. Type guards становятся ключевым инструментом для безопасного взаимодействия с форматтерами и парсерами.


Особенности типизации входных данных Globalize

Основные функции Globalize работают с тремя категориями данных:

  • числа (number)
  • даты (Date)
  • строки (string, при парсинге)

Примеры API:

  • Globalize.numberFormatter()
  • Globalize.dateFormatter()
  • Globalize.parseNumber()
  • Globalize.parseDate()

Типичная проблема заключается в том, что источники данных не гарантируют корректный тип. Например, значение может приходить из API как unknown:

const valueFromApi: unknown = getValue();

Перед передачей в Globalize требуется уточнение типа.


Базовые type guards для чисел

Наиболее распространённый guard — проверка на число:

function isNumber(value: unknown): value is number {
  return typeof value === "number" && !Number.isNaN(value);
}

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

const formatter = Globalize.numberFormatter({ maximumFractionDigits: 2 });

function formatSafe(value: unknown) {
  if (isNumber(value)) {
    return formatter(value);
  }
  return "";
}

Особенность: Globalize ожидает корректный number, иначе поведение может быть непредсказуемым (например, строковая конкатенация или NaN в результате форматирования).


Type guards для строк и парсинга чисел

При работе с parseNumber входные данные должны быть строкой:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

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

const parser = Globalize.numberParser();

function parseSafe(value: unknown) {
  if (isString(value)) {
    return parser(value);
  }
  return null;
}

Парсинг чисел в локализованных форматах зависит от текущей культуры (locale), поэтому некорректный тип строки приводит к ошибкам интерпретации.


Type guards для дат

Globalize работает с датами через объекты Date, поэтому требуется строгая проверка:

function isDate(value: unknown): value is Date {
  return value instanceof Date && !isNaN(value.getTime());
}

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

const dateFormatter = Globalize.dateFormatter({ datetime: "medium" });

function formatDateSafe(value: unknown) {
  if (isDate(value)) {
    return dateFormatter(value);
  }
  return "";
}

Сужение типов перед форматированием

В TypeScript часто применяется объединённый тип:

type Input = string | number | Date | null | undefined;

Комбинированные type guards позволяют последовательно сузить тип:

function isValidInput(value: unknown): value is string | number | Date {
  return isString(value) || isNumber(value) || isDate(value);
}

Дальнейшая обработка:

function format(value: unknown) {
  if (!isValidInput(value)) {
    return "";
  }

  if (isNumber(value)) {
    return Globalize.numberFormatter()(value);
  }

  if (isString(value)) {
    return Globalize.numberParser()(value);
  }

  return Globalize.dateFormatter()(value);
}

Такой подход отражает типичную модель работы Globalize: один API, несколько доменных типов данных.


Guard для результатов парсинга Globalize

Парсеры Globalize могут возвращать number | null или Date | null, что требует дополнительной проверки:

function isParsedNumber(value: unknown): value is number {
  return typeof value === "number" && !Number.isNaN(value);
}

Пример:

const parser = Globalize.numberParser();

function safeParse(value: unknown): number | null {
  if (!isString(value)) return null;

  const result = parser(value);

  if (isParsedNumber(result)) {
    return result;
  }

  return null;
}

Пользовательские type guards для локализованных форматов

При работе с форматами, зависящими от культуры (cldr, locale), часто требуется дополнительная логика:

type LocaleNumber = string;

function isLocaleNumber(value: unknown): value is LocaleNumber {
  return (
    typeof value === "string" &&
    /^[\d\s.,\u00A0]+$/.test(value)
  );
}

Применение:

const parser = Globalize.numberParser();

function parseLocaleNumber(value: unknown) {
  if (!isLocaleNumber(value)) return null;

  return parser(value);
}

Type guards и безопасность цепочек преобразований

Globalize часто используется в цепочках:

  1. получение данных
  2. парсинг
  3. форматирование

Без type guards цепочка становится уязвимой:

const formatted = Globalize.numberFormatter()(value);

С guards:

function process(value: unknown) {
  if (!isNumber(value)) return null;

  const formatted = Globalize.numberFormatter()(value);

  return formatted;
}

Согласование type guards с CLDR-данными

Globalize опирается на CLDR, где локализация влияет на:

  • разделители тысяч
  • десятичные символы
  • форматы дат
  • правила множественных форм

Type guards в этом контексте не заменяют валидацию CLDR, но ограничивают входной домен, снижая риск некорректной интерпретации локализованных строк.


Композиция type guards для масштабируемых систем

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

function isPrimitive(value: unknown): value is string | number | Date {
  return isString(value) || isNumber(value) || isDate(value);
}

function isGlobalizeSafe(value: unknown): value is string | number {
  return isString(value) || isNumber(value);
}

Такая структура позволяет адаптировать входные данные под разные API Globalize без дублирования логики.


Интеграция type guards в слой форматирования

Форматирование часто выносится в отдельный слой:

class FormatterService {
  private numberFormatter = Globalize.numberFormatter();
  private dateFormatter = Globalize.dateFormatter();

  formatNumber(value: unknown) {
    if (!isNumber(value)) return "";
    return this.numberFormatter(value);
  }

  formatDate(value: unknown) {
    if (!isDate(value)) return "";
    return this.dateFormatter(value);
  }
}

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


Ошибки, возникающие при отсутствии type guards

Без проверки типов характерны следующие проблемы:

  • передача string в numberFormatter без парсинга
  • NaN в результате вычислений
  • некорректная интерпретация дат (Invalid Date)
  • непредсказуемые результаты при локализации строк

Type guards устраняют неопределённость на границе между данными и Globalize API, обеспечивая детерминированное поведение форматтеров и парсеров.