TypeScript augmentation
next-intl
integrates seamlessly with TypeScript right out of the box, requiring no additional setup.
However, you can optionally provide supplemental definitions to augment the types that next-intl
works with, enabling improved autocompletion and type safety across your app.
import {routing} from '@/i18n/routing';
import {formats} from '@/i18n/request';
import messages from './messages/en.json';
declare module 'next-intl' {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof messages;
Formats: typeof formats;
}
}
Type augmentation is available for:
Locale
Augmenting the Locale
type will affect all APIs from next-intl
that either return or receive a locale:
import {useLocale} from 'next-intl';
// ✅ 'en' | 'de'
const locale = useLocale();
import {Link} from '@/i18n/routing';
// ✅ Passes the validation
<Link href="/" locale="en" />;
Additionally, next-intl
provides a Locale
type that can be used when passing the locale as an argument.
To enable this validation, you can adapt AppConfig
as follows:
import {routing} from '@/i18n/routing';
declare module 'next-intl' {
interface AppConfig {
// ...
Locale: (typeof routing.locales)[number];
}
}
Messages
Messages can be strictly typed to ensure you’re using valid keys.
{
"About": {
"title": "Hello"
}
}
function About() {
// ✅ Valid namespace
const t = useTranslations('About');
// ✖️ Unknown message key
t('description');
// ✅ Valid message key
t('title');
}
To enable this validation, you can adapt AppConfig
as follows:
import messages from './messages/en.json';
declare module 'next-intl' {
interface AppConfig {
// ...
Messages: typeof messages;
}
}
You can freely define the interface, but if you have your messages available locally, it can be helpful to automatically create the type based on the messages from your default locale.
Does this affect the performance of type checking?
While the size of your messages file can have an effect on the time it takes to run the TypeScript compiler on your project, the overhead of augmenting Messages
should be reasonably fast.
Here’s a benchmark from a sample project with 340 messages:
- No type augmentation for messages: ~2.20s
- Type-safe keys: ~2.82s
- Type-safe arguments: ~2.85s
This was observed on a MacBook Pro 2019 (Intel).
If you experience performance issues on larger projects, you can consider:
- Using type augmentation of messages only on your continuous integration pipeline as a safety net
- Splitting your project into multiple packages in a monorepo, allowing you to work with separate messages per package
Does this affect the performance of my editor?
Generally, type augmentation for Messages
should be reasonably fast.
In case you notice your editor performance related to saving files to be impacted, it might be caused by running ESLint on save when using type-aware rules from @typescript-eslint
.
To ensure your editor performance is optimal, you can consider running expensive, type-aware rules only on your continuous integration pipeline:
// ...
// Run expensive, type-aware linting only on CI
'@typescript-eslint/no-misused-promises': process.env.CI
? 'error'
: 'off'
Type-safe arguments
Apart from strictly typing message keys, you can also ensure type safety for message arguments:
{
"UserProfile": {
"title": "Hello {firstName}"
}
}
function UserProfile({user}) {
const t = useTranslations('UserProfile');
// ✖️ Missing argument
t('title');
// ✅ Argument is provided
t('title', {firstName: user.firstName});
}
TypeScript currently has a limitation where it infers values of imported JSON modules as loose types like string
instead of the actual value. To bridge this gap for the time being, next-intl
can generate an accompanying .d.json.ts
file for the messages that you’re assigning to your AppConfig
.
Usage:
- Add support for JSON type declarations in your
tsconfig.json
:
{
"compilerOptions": {
// ...
"allowArbitraryExtensions": true
}
}
- Configure the
createMessagesDeclaration
setting in your Next.js config:
import {createNextIntlPlugin} from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin({
experimental: {
// Provide the path to the messages that you're using in `AppConfig`
createMessagesDeclaration: './messages/en.json'
}
// ...
});
// ...
With this setup in place, you’ll see a new declaration file generated in your messages
directory once you run next dev
or next build
:
messages/en.json
+ messages/en.d.json.ts
This declaration file will provide the exact types for the JSON messages that you’re importing and assigning to AppConfig
, enabling type safety for message arguments.
To keep your code base tidy, you can ignore this file in Git:
messages/*.d.json.ts
Please consider upvoting TypeScript#32063
to potentially remove this workaround in the future.
Formats
If you’re using global formats, you can strictly type the format names that are referenced in calls to format.dateTime
, format.number
and format.list
.
function Component() {
const format = useFormatter();
// ✖️ Unknown format string
format.dateTime(new Date(), 'unknown');
// ✅ Valid format
format.dateTime(new Date(), 'short');
// ✅ Valid format
format.number(2, 'precise');
// ✅ Valid format
format.list(['HTML', 'CSS', 'JavaScript'], 'enumeration');
}
To enable this validation, export the formats that you’re using e.g. from your request configuration:
import {Formats} from 'next-intl';
export const formats = {
dateTime: {
short: {
day: 'numeric',
month: 'short',
year: 'numeric'
}
},
number: {
precise: {
maximumFractionDigits: 5
}
},
list: {
enumeration: {
style: 'long',
type: 'conjunction'
}
}
} satisfies Formats;
// ...
Now, you can include the formats
in your AppConfig
:
import {formats} from '@/i18n/request';
declare module 'next-intl' {
interface AppConfig {
// ...
Formats: typeof formats;
}
}
Troubleshooting
If you’re encountering problems, double check that:
- The interface uses the correct name
AppConfig
. - Your type declaration file is included in
tsconfig.json
. - Your editor has loaded the latest types. When in doubt, restart your editor.