Upgrade to React Intl 12#
React Intl 11 was an unstable transition release while the typed ICU argument API was being finalized. Upgrade directly to the latest 12.x patch release. This guide combines the changes introduced in 11 and 12 so applications on 10.x or the unstable 11.x releases have one migration path.
The empty-contract helper defaults are a correction to the 12.x API, shipped in a patch release. They can surface new TypeScript errors in helper-based messages with arguments; run the autofix migration below before updating callers manually. JavaScript formatting behavior is unchanged.
Run the type-generation autofix#
Enable formatjs/enforce-message-types with generateTypes: true in your linter
configuration and run autofix. It derives argument contracts from the ICU strings,
updates existing helper annotations, and removes redundant empty generics.
{
"rules": {
"formatjs/enforce-message-types": ["error", {"generateTypes": true}]
}
}
For example:
// No arguments: the helper defaults to an empty contract.
const hello = defineMessage({defaultMessage: 'Hello'})
// The linter generates the contract for messages with arguments.
const count = defineMessage<{readonly count: number | bigint}>({
defaultMessage: '{count, number} items',
})
intl.$t(hello)
intl.$t(count, {count: 2})
Keep the rule enabled so contracts follow later message edits. Review diagnostics that cannot be autofixed, such as dynamic message text or unsafe catalog lookups.
API changes from 10.x#
TypeScript 5.4 required#
Update TypeScript to 5.4 or newer. The declarations now use NoInfer for
opt-in typed message contracts. This compiler requirement also applies to
projects that use untyped messages; JavaScript runtime behavior is unchanged.
Message formatter generic arguments changed#
formatMessage<Values, RichOutput> and $t<Values, RichOutput> now take the ICU
argument contract first. Rich output moves to the optional second generic,
which defaults to React.ReactNode in React Intl.
// Before: first generic described rich output.
intl.$t<React.ReactNode>(descriptor, values)
// After: first generic describes ICU arguments.
intl.$t<{name: MessageValue}, React.ReactNode>(descriptor, values)
Import MessageValue from react-intl for unformatted placeholders and
MessageTag for tag callbacks.
With a plain descriptor, both
intl.$t(descriptor, values) and intl.formatMessage(descriptor, values)
remain permissive: required argument names and types are not inferred from
message literals. A descriptor from a typed helper still enforces its contract
without call-site generics. Runtime formatting and error handling are unchanged.
Rich output defaults to React.ReactNode in React Intl. In @formatjs/intl,
it uses the type supplied to createIntl<T>() (string by default); untyped
rich calls use that base type rather than inferring a narrower output type.
See React Intl argument types and core Intl argument types for examples.
Readonly message helpers#
defineMessage returns a readonly descriptor. defineMessages returns a readonly
catalog of readonly descriptors, including calls without typed options. Construct
a new descriptor or catalog instead of mutating a helper result. This is a
TypeScript breaking change; helpers preserve object identity and do not freeze
objects or recursively transform rich values.
Empty helper contracts by default#
defineMessage({defaultMessage: 'Hello'}) now carries the same empty ICU
contract as defineMessage<{}>(...). defineMessages defaults each entry to
an empty contract, and TypedMessageDescriptor defaults its type argument to
{}. Formatting these messages rejects extra values without explicit generics.
Helpers for messages with arguments need an explicit contract. Run
formatjs/enforce-message-types with generateTypes: true to generate these
contracts and remove redundant empty generics. This changes TypeScript checking
only; descriptors keep their runtime identity and formatting behavior.
Typed helper metadata#
Typed helpers retain required id and defaultMessage fields for common
descriptor shapes. Catalog inference retains fields shared by every entry.
For literal IDs, custom metadata, or heterogeneous catalogs, supply a second
descriptor generic: defineMessage<Values, typeof descriptor>(descriptor)
or defineMessages<Contracts, typeof catalog>(catalog).
TypeScript cannot partially infer that second generic after an explicit first one.
ESLint refreshes the ICU generic while preserving the metadata generic.
The two-generic typed catalog overload reserves defineMessages<Contracts, Descriptors>.
Legacy instantiation expressions using defineMessages<Key, Descriptor> should
supply their existing third generic explicitly:
defineMessages<Key, Descriptor, Record<Key, Descriptor>>.
Calls without explicit generics retain descriptor metadata and now carry an empty
contract, as described above.
Typed FormattedMessage#
Spreading a typed descriptor into FormattedMessage now checks its values:
<FormattedMessage {...messages.count} values={{count: 2}} />.
Required ICU arguments require the values prop, and rich tags require callbacks
returning React nodes. Argument-free descriptors can omit values. Ordinary
untyped JSX remains permissive. Keep the descriptor's phantom contract when
passing it through wrappers; widening to MessageDescriptor erases the check.
Runtime rendering, memoization, and children callbacks are unchanged.
Registered message arguments#
Applications can opt into ID-only checks by extending
FormatjsIntl.MessageArguments. Both formatMessage and $t use the map
in @formatjs/intl and React Intl:
declare global {
namespace FormatjsIntl {
interface MessageArguments {
'cart.total': {readonly count: number | bigint}
'cart.empty': {}
}
}
}
intl.$t({id: 'cart.total'}, {count: 2})
intl.$t({id: 'cart.empty'})
// Type error: count is required.
intl.$t({id: 'cart.total'})
Known literal IDs check required arguments, empty contracts, and rich callbacks.
Union IDs require values valid for every possible registered message.
Dynamic strings and unregistered IDs retain legacy behavior; this map does not
replace the separate FormatjsIntl.Message.ids restriction.
Explicit argument generics and typed descriptors keep their own contracts.
No ICU parsing, registration, or freezing happens at runtime.
For catalogs already carrying generated contracts, use
MessageArgumentsFromCatalog<typeof messages> from either package:
import type {MessageArgumentsFromCatalog} from 'react-intl'
import type {messages} from './messages'
type AppMessageArguments = MessageArgumentsFromCatalog<typeof messages>
declare global {
namespace FormatjsIntl {
interface MessageArguments extends AppMessageArguments {}
}
}
Catalog entries must retain literal IDs. Use the helpers' explicit descriptor
metadata generic when needed; widened string IDs are omitted. Keep IDs unique:
duplicate catalog IDs combine their contracts. This API consumes existing typed
catalogs or application declarations; it does not add a CLI declaration generator.