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.