Типизация с TypeScript

Базовая модель типов в экосистеме интернационализации

FormatJS представляет собой набор библиотек для интернационализации JavaScript-приложений, где ключевая идея заключается в строгом разделении сообщений, форматирования и локализации данных. В контексте TypeScript это приводит к необходимости описывать типы для сообщений, параметров форматирования, локалей и runtime-объектов Intl.

Типизация в экосистеме FormatJS строится вокруг нескольких фундаментальных слоёв:

  • типы сообщений (message descriptors)
  • типы параметров форматирования
  • типы Intl API
  • типы компонентов (в случае React-обвязки react-intl)
  • типы компиляции сообщений (babel/CLI инструменты)

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


Типизация сообщений (MessageDescriptor)

Основной тип, используемый для описания интернационализируемых строк, — MessageDescriptor. Он определяет структуру сообщения, которое передаётся в форматтеры или компоненты.

Типовая структура:

import { MessageDescriptor } from 'react-intl';

const message: MessageDescriptor = {
  id: 'user.profile.title',
  defaultMessage: 'User profile',
  description: 'Title of the user profile page'
};

Ключевые поля:

  • id — уникальный идентификатор сообщения
  • defaultMessage — fallback-строка
  • description — пояснение для переводчиков

Расширенная типизация сообщений

В больших системах часто вводится строгая модель ID:

type MessageIds =
  | 'auth.login.title'
  | 'auth.login.button'
  | 'dashboard.welcome';

const msg: MessageDescriptor = {
  id: 'auth.login.title' satisfies MessageIds,
  defaultMessage: 'Login'
};

Такой подход предотвращает появление «плавающих» строковых идентификаторов и обеспечивает контроль на уровне компилятора.


Типизация значений для интерполяции

FormatJS активно использует интерполяцию значений в сообщениях ICU-формата. В TypeScript это требует строгого описания параметров.

Пример сообщения:

const message = {
  id: 'greeting',
  defaultMessage: 'Hello, {name}!'
};

Типизация параметров:

type GreetingValues = {
  name: string;
};

Использование с форматтером:

import { IntlShape } from 'react-intl';

function formatGreeting(intl: IntlShape, values: GreetingValues) {
  return intl.formatMessage(
    { id: 'greeting', defaultMessage: 'Hello, {name}!' },
    values
  );
}

Строгая привязка ключей через generics

В современных архитектурах вводится слой генерации типов сообщений:

type AppMessages = {
  greeting: { name: string };
  unread: { count: number };
};

И использование через обобщения:

function format<K extends keyof AppMessages>(
  key: K,
  values: AppMessages[K]
) {
  return key;
}

Типизация pluralization и ICU-форматов

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

Пример:

const message = {
  id: 'cart.items',
  defaultMessage: 'You have {count, plural, one {# item} other {# items}}'
};

Типизация:

type CartValues = {
  count: number;
};

Важно учитывать:

  • plural всегда требует number
  • sel ect выражения требуют union-типы
  • date/time форматирование требует Date | number

Типизация форматтеров (IntlShape)

Ключевая сущность в react-intl — IntlShape, описывающая runtime API.

Основные методы:

intl.formatMessage
intl.formatDate
intl.formatNumber
intl.formatTime

Типизация formatMessage

interface IntlShape {
  formatMessage: <T extends Record<string, any>>(
    descriptor: MessageDescriptor,
    values?: T
  ) => string;
}

Это позволяет связывать сообщение и параметры через generic.


Типизация чисел и дат

Числовые значения

intl.formatNumber(1234.56, {
  style: 'currency',
  currency: 'USD'
});

TypeScript-тип:

type NumberFormatOptions = Intl.NumberFormatOptions;

Под капотом используется стандартный Intl.NumberFormatOptions.


Даты

intl.formatDate(new Date(), {
  year: 'numeric',
  month: 'long',
  day: '2-digit'
});

Типизация:

type DateFormatOptions = Intl.DateTimeFormatOptions;

Типизация сообщений через Codegen

В современных проектах FormatJS часто используется генерация типов из исходных сообщений.

Пример входного JSON:

{
  "app.title": "Application",
  "app.welcome": "Welcome {name}"
}

Сгенерированный TypeScript:

export interface IntlMessages {
  'app.title': void;
  'app.welcome': {
    name: string;
  };
}

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

  • проверять ключи сообщений
  • валидировать параметры интерполяции
  • исключать несуществующие переводы

Типизация в Babel-пайплайне FormatJS

FormatJS включает инструменты компиляции сообщений через Babel-плагины, где TypeScript играет роль слоя валидации поверх трансформаций.

Пример конфигурации:

module.exports = {
  plugins: [
    [
      'formatjs',
      {
        removeDefaultMessage: false,
        extractFromFormatMessageCall: true
      }
    ]
  ]
};

TypeScript помогает гарантировать, что:

  • id всегда строка
  • defaultMessage соответствует ICU синтаксису
  • значения параметров совпадают с типами

Типизация react-intl компонентов

FormattedMessage

import { FormattedMessage } fr om 'react-intl';

<FormattedMessage
  id="welcome"
  defaultMessage="Welcome {name}"
  values={{ name: 'Alex' }}
/>;

Типизация:

interface FormattedMessageProps<T = Record<string, any>> {
  id: string;
  defaultMessage?: string;
  values?: T;
}

Формирование строгих пропсов

type WelcomeValues = {
  name: string;
};

type WelcomeProps = {
  values: WelcomeValues;
};

Типизация локалей

Локаль в FormatJS обычно представлена строкой, но в строгих системах вводится union:

type Locale = 'en' | 'ru' | 'de' | 'fr';

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

function setLocale(locale: Locale) {
  return locale;
}

Это предотвращает передачу некорректных регионов и снижает runtime-ошибки.


Типизация fallback-механизмов

Fallback используется при отсутствии перевода:

type FallbackConfig = {
  defaultLocale: Locale;
  messages: Record<string, string>;
};

Пример:

const config: FallbackConfig = {
  defaultLocale: 'en',
  messages: {}
};

Строгая интеграция с TypeScript в больших приложениях

В масштабных системах вводится единая модель:

type IntlDictionary = Record<string, Record<string, any>>;

И специализированные утилиты:

type ExtractValues<T> = T extends Record<string, infer V> ? V : never;

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

  • унифицировать типы сообщений
  • централизовать контроль переводов
  • связывать UI и локализацию

Типизация ошибок и runtime-safe слой

FormatJS не полностью предотвращает runtime-ошибки, поэтому вводятся дополнительные типы:

type IntlError =
  | 'MISSING_MESSAGE'
  | 'INVALID_FORMAT'
  | 'MISSING_VALUE';

Обработка:

function handleIntlError(error: IntlError) {
  return error;
}

Композиция типизации в экосистеме FormatJS

Вся система типов формируется как композиция:

  • базовые Intl типы (ECMAScript)
  • типы сообщений FormatJS
  • типы React компонентов
  • пользовательские доменные типы

В результате формируется строгая модель интернационализации, где каждая строка, параметр и формат подчинены контрактам TypeScript.