Upgrade to React Intl 12#
React Intl 11 was an unstable transition release. Upgrade from 10 or 11 to the latest 12.x patch and use TypeScript 5.4 or newer. JavaScript formatting behavior is unchanged.
Generate message types#
Enable this ESLint rule and run eslint --fix:
{
"rules": {
"formatjs/enforce-message-types": ["error", {"generateTypes": true}]
}
}
The rule reads your ICU messages and adds their argument types:
// Before
const items = defineMessage({defaultMessage: '{count, number} items'})
// After autofix
const items = defineMessage<{readonly count: number | bigint}>({
defaultMessage: '{count, number} items',
})
intl.$t(items, {count: 2}) // OK
intl.$t(items, {count: 'two'}) // Type error
intl.$t(items) // Type error: count is required
Messages without arguments need no generic: defineMessage({defaultMessage: 'Hello'}) rejects extra values by default. Autofix removes redundant <{}>.
Messages with arguments need the generated types, including when upgrading from
an earlier 12.x patch.
Keep the rule enabled to update types when messages change. Fix any remaining diagnostics manually; see the rule documentation for supported patterns.
Check these breaking changes#
Formatter generics now describe message arguments#
The first generic of formatMessage and $t now describes ICU arguments.
Rich output moves to the second generic and defaults to React.ReactNode:
// Before
intl.$t<React.ReactNode>(descriptor, values)
// After
intl.$t<{name: MessageValue}, React.ReactNode>(descriptor, values)
Import MessageValue from react-intl. With typed defineMessage or
defineMessages results, omit call-site generics—the descriptor supplies them.
Plain, untyped descriptors remain permissive.
Helper results are readonly#
Create a new descriptor or catalog instead of mutating the result of
defineMessage or defineMessages. This is a TypeScript restriction; objects
are not frozen at runtime.
If you need literal IDs or custom metadata alongside explicit argument types,
use defineMessage<Values, typeof descriptor>(descriptor) or
defineMessages<Contracts, typeof catalog>(catalog). TypeScript cannot infer
the second generic after you supply the first.
Legacy defineMessages<Key, Descriptor> instantiation expressions need the
third generic: defineMessages<Key, Descriptor, Record<Key, Descriptor>>.
Typed FormattedMessage checks values#
<FormattedMessage {...items} values={{count: 2}} /> checks the same argument
types as $t. Required arguments need values; rich tags need callbacks.
Avoid widening typed descriptors to MessageDescriptor, which loses these checks.
For ID-only calls, you can optionally register message arguments.