В экосистеме FormatJS интерполяция реализуется через ICU MessageFormat — формат строк, поддерживающий именованные параметры, множественные формы, выбор и вложенные конструкции.
Типичный шаблон сообщения:
{
greeting: "Привет, {name}"
}
Интерполяция выполняется через передачу объекта значений:
intl.formatMessage(
{ id: "greeting", defaultMessage: "Привет, {name}" },
{ name: "Алексей" }
)
Ключевая проблема на этом уровне — отсутствие гарантий соответствия между шаблоном и передаваемыми значениями. Ошибки проявляются только во время выполнения: опечатки в ключах, неправильные типы, пропущенные параметры.
Типобезопасность в FormatJS достигается через статическое описание сообщений и генерацию типов.
Основная модель строится вокруг MessageDescriptor:
import { MessageDescriptor } from "react-intl";
const messages: Record<string, MessageDescriptor> = {
greeting: {
id: "greeting",
defaultMessage: "Привет, {name}"
}
};
Однако использование
Record<string, MessageDescriptor> ослабляет строгую
проверку ключей. Более строгий подход — сохранение литеральных
типов.
Ключевым элементом типобезопасности становится фиксация структуры
сообщений через as const:
export const messages = {
greeting: {
id: "greeting",
defaultMessage: "Привет, {name}"
},
unread: {
id: "unread",
defaultMessage: "Новых сообщений: {count}"
}
} as const;
Такой подход сохраняет:
typeofНа основе ICU-шаблонов возможно построение типов значений.
Пример базовой типизации:
type MessageValues = {
greeting: { name: string };
unread: { count: number };
};
Использование:
function format<K extends keyof MessageValues>(
key: K,
values: MessageValues[K]
) {
return intl.formatMessage(messages[key], values);
}
Данный подход обеспечивает:
Ручное описание типов не масштабируется, поэтому применяется генерация через CLI инструменты FormatJS.
Используется @formatjs/cli:
formatjs extract "src/**/*.{ts,tsx}" --out-file messages.json
Далее генерируются TypeScript-типы:
formatjs compile messages.json --out-file messages.ts
Результат включает строго типизированные ключи и значения интерполяции.
defineMessagesБолее интегрированный подход основан на
defineMessages:
import { defineMessages } from "react-intl";
const messages = defineMessages({
greeting: {
id: "greeting",
defaultMessage: "Привет, {name}"
},
unread: {
id: "unread",
defaultMessage: "Новых сообщений: {count}"
}
});
TypeScript способен извлечь структуру:
type Keys = keyof typeof messages;
Однако типизация значений интерполяции требует дополнительной обвязки или генераторов.
Для повышения точности применяется извлечение параметров из строк ICU.
Пример утилитарного типа:
type ExtractParams<S extends string> =
S extends `${string}{${infer Param}}${infer Rest}`
? Param | ExtractParams<Rest>
: never;
Использование:
type GreetingMessage = "Привет, {name}";
type GreetingParams = Record<ExtractParams<GreetingMessage>, string>;
Результат:
name становится обязательным ключомОшибка возникает при передаче неверного типа:
intl.formatMessage(messages.unread, {
count: "пять" // ошибка: ожидается number
});
Типизация значений устраняет класс ошибок:
ICU поддерживает опциональные значения через условия и fallback:
{
welcome: "Привет, {name, select, undefined {гость} other {{name}}}"
}
Типизация таких конструкций усложняется, поскольку значение может быть:
В строгих схемах используется расширенный тип:
type NullableParams<T> = {
[K in keyof T]?: T[K] | undefined;
};
FormatJS различает интерполяцию и форматирование.
Пример:
{
price: "Цена: {value, number, currency}"
}
Типизация:
type PriceParams = {
value: number;
};
Важно разделение:
number)currency, percent,
unit)Формат не влияет на тип, но влияет на runtime-обработку.
intl.formatMessageСтандартная сигнатура:
intl.formatMessage(descriptor, values?)
Проблемы:
values имеет тип
Record<string, any>Решение — обёртка с дженериками:
function typedFormat<K extends keyof MessageValues>(
key: K,
values: MessageValues[K]
) {
return intl.formatMessage(messages[key], values);
}
В контексте React используется:
import { useIntl } fr om "react-intl";
const Component = () => {
const intl = useIntl();
return intl.formatMessage(messages.greeting, {
name: "Мария"
});
};
При строгой типизации формируется контракт:
Более надёжный метод типизации основан на AST-парсинге ICU-строк.
FormatJS предоставляет парсер сообщений, формирующий структуру:
MessageFormatElementLiteralElementArgumentElementSelectElementPluralElementИз этой структуры извлекаются аргументы:
import { parse } from "@formatjs/icu-messageformat-parser";
Далее возможно построение строгих типов на основе AST:
ArgumentElementТипобезопасность в FormatJS не покрывает полностью динамические сценарии:
В этих случаях применяется частичная типизация:
type SafeValues = Record<string, string | number | boolean | null>;
Наиболее устойчивый подход строится на связке:
as const для локальных модулейformatMessageРезультат — замкнутый контур типизации: