В базовой конфигурации i18next система перевода работает с ключами
строкового типа. Функция t принимает произвольную
строку:
t('common.save')
t('errors.notFound')
Такой подход не ограничивает разработчика и допускает ряд проблем:
Строгая типизация решает эти проблемы, превращая ключи переводов в часть контрактной системы приложения.
Основной принцип заключается в том, чтобы представить структуру переводов как тип данных TypeScript, а ключи — как объединение строковых литералов.
Пример ресурса переводов:
export const resources = {
en: {
common: {
save: "Save",
cancel: "Cancel"
},
errors: {
notFound: "Not found"
}
}
} as const;
Ключевое значение здесь — as const, которое делает
структуру неизменяемой и позволяет вывести точные литералы.
Далее можно получить тип ключей:
type Resources = typeof resources;
Однако этого недостаточно для полноценной типизации t,
так как i18next работает с вложенными namespace и языками.
Для извлечения всех возможных путей используется рекурсивный тип:
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}.${P}`
: never
: never;
type Paths<T> = {
[K in keyof T]: T[K] extends object
? Join<K, Paths<T[K]>>
: K
}[keyof T];
Применение:
type TranslationKeys = Paths<typeof resources.en>;
Результат:
"common.save" | "common.cancel" | "errors.notFound"
Теперь любые обращения к t можно ограничить этим
типом.
i18next позволяет расширять типы через declaration merging.
Создаётся файл i18next.d.ts:
import 'i18next';
import type { resources } fr om './resources';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'common';
resources: typeof resources['en'];
}
}
После этого TypeScript начинает связывать ключи с реальной структурой ресурсов.
При правильной настройке можно добиться следующего поведения:
t('common.save'); // корректно
t('common.unknown'); // ошибка TypeScript
t('errros.notFound'); // ошибка TypeScript (опечатка)
Типизация предотвращает даже корректные, но отсутствующие ключи.
В i18next часто используется разделение на namespace:
{
en: {
common: {...},
auth: {...}
}
}
Для этого вводится дополнительная структура:
type AppResources = typeof resources['en'];
type Namespaces = keyof AppResources;
И расширенная форма ключей:
type NamespaceKeys<N extends keyof AppResources> =
Paths<AppResources[N]>;
Использование:
type CommonKeys = NamespaceKeys<'common'>;
В связке с React используется react-i18next, который
также поддерживает типизацию через module augmentation.
import 'react-i18next';
import type resources from './resources';
declare module 'react-i18next' {
interface Resources {
en: typeof resources['en'];
}
}
После этого хук useTranslation начинает строго проверять
ключи:
const { t } = useTranslation();
t('common.save'); // корректно
t('common.svae'); // ошибка
i18next поддерживает параметры:
t('welcome', { name: 'John' })
Без типизации параметры остаются any. Для строгого
контроля можно описать интерфейсы:
interface InterpolationMap {
'welcome': { name: string };
'errors.lim it': { max: number };
}
И связать их с перегрузкой:
type TypedTFunction = <
K extends keyof InterpolationMap
>(
key: K,
options: InterpolationMap[K]
) => string;
Теперь ошибка будет выбрасываться при неправильных параметрах:
t('welcome', { name: 'John' }); // ok
t('welcome', { age: 20 }); // ошибка
i18next позволяет задавать defaultNS, и это влияет на
поведение ключей.
interface CustomTypeOptions {
defaultNS: 'common';
}
Это позволяет писать:
t('save'); // интерпретируется как common.save
Но при строгой типизации требуется контроль:
Для крупных проектов используется генерация типов через утилиты:
type CreateKeys<T, Prefix extends string = ''> = {
[K in keyof T]: T[K] extends object
? CreateKeys<T[K], `${Prefix}${K & string}.`>
: `${Prefix}${K & string}`
}[keyof T];
Результат — единая система ключей без ручного поддержания типов.
Строгая типизация позволяет выявлять:
Комбинация типов и ESLint rule no-undefined-keys делает
систему самопроверяемой.
Несмотря на высокую точность, существуют ограничения:
t(\error.${code})) теряют строгую
типизациюДля динамических случаев вводится частичное ослабление:
t(`error.${string}`)
что сохраняет баланс между гибкостью и безопасностью.
В крупных системах применяется комбинированный подход:
Такой подход позволяет сохранить строгую проверяемость без потери гибкости i18next.