react-i18next: Cannot pass objects to `` with React 18 type definitions

🐛 Bug Report

With version 18 of @types/react, Typescript complains when passing objects to the Trans component.

It previously worked because ReactNode had a | {} in its definition, allowing arbitrary objects to be passed. That has been removed (for a good reason) in version 18 of the type definitions. See this PR for more information.

To Reproduce

From this example:

<Trans i18nKey="userMessagesUnread" count={count}>
  Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>

This fails type checking: TS2322: Type '{ name: void; }' is not assignable to type 'ReactNode'.

Expected behavior

Type checking works, i.e. the TS compiler does not complain.

I would think that {} (or something like Record<string, unknown>) should be explicitly added to the children type of Trans.

Your Environment

  • react version: 18.0.0
  • i18next version: 11.16.5
  • TS version: 4.6.3

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 21
  • Comments: 35 (15 by maintainers)

Commits related to this issue

Most upvoted comments

This is still a problem. Please don’t close. (I’m sorry but I can’t think of a single reason why stalebot would be useful…)

Came across this issue as well, here is how I resolved it:

<Trans
    ns="NAMESPACE"
    i18nKey="KEY"
    values={{ lower, upper }}
    components={{ 1: <strong />, 3: <strong /> }}
  >
    {`Consider <1>{{lower}}</1> to <3>{{upper}}</3> magnification.`}
  </Trans>

With the translation using the same <1></1> and <3></3>.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

For anyone stumbling across this in the future and needs to figure out how to use the allowObjectInHTMLChildren option, you just have to create a type declaration file like src/types/react-i18next.d.ts and then add the following to it

import 'react-i18next';

declare module 'react-i18next' {
  interface CustomTypeOptions {
    allowObjectInHTMLChildren: true;
  }
}

In case it helps anyone else: Using allowObjectInHTMLChildren felt fairly drastic (since it changes the allowed prop types of all React components, whether or not they’re part of <Trans>), and it isn’t perfect (#1543), so I’ve started using typecasts instead:

<Trans i18nKey="userMessagesUnread" count={count}>
  Hello <strong title={t('nameTitle')}>{{name} as any}</strong>, you have {{count}} unread message.
</Trans>

I use a type alias to make this more self-documenting:

/**
 *  Interpolation for `t` - see https://github.com/i18next/react-i18next/issues/1483
 */
export type TI = any;
// ...

return (
  <Trans i18nKey="userMessagesUnread" count={count}>
    Hello <strong title={t('nameTitle')}>{{name} as TI}</strong>, you have {{count}} unread message.
  </Trans>
);

(This relies on new work in i18next-parser; see https://github.com/i18next/i18next-parser/issues/603.)

This issue still exists even with 12.2.0 , I’m hoping there will be a fix for this. Making use of any is not the workaround I want to go for

Using the suggested .d.ts mod from @dave-multiplier breaks all my other JSX tags with errors like:

error TS2746: This JSX tag's 'children' prop expects a single child of type 'ReactI18NextChild | Iterable<React
I18NextChild>', but multiple children were provided.

The above error is thrown for elements like:

<label htmlFor={`${id}-phone`}>
    {t('Phone Number')}
    <RequiredMark />
</label>

Wrapping {t('Phone Number')} inside a span tag fixes the problem.

For anyone landing here today, this worked fine for me:

    <Trans
      t={t}
      values={{ minutesToLogout, secondsToLogout }}
      components={[<span className={minutesToLogout === 0 ? styles.danger : ''} key="dummy-key" />]}
    >
      {'You will be signed out in <0>{{ minutesToLogout }}m {{ secondsToLogout }}s</0>'}
    </Trans>

where the key is automatically detected as 'You will be signed out in <0>{{ minutesToLogout }}m {{ secondsToLogout }}s</0>'

v11.16.8 offers allowObjectInHTMLChildren option

It’s not a runtime issue…as (not like typescript) javascript is just code 😉…react’s JSX is nothing but a stack of function calls…the JSX element’s children is just an array of objects. So we take that {{notAllowedObjectInChildren}} and make what we need with it -> parse it to interpolation values.

That way we can use i18next JSON interpolation syntax inside JSX elements…it’s somewhat clever but has its drawbacks with over-restrictive typescript…if it’s good or bad thing might depend on the user.

If typescript users prefer not extending this…the other option is to not allow using Trans in that way -> but use it like for ICU syntax https://react.i18next.com/misc/using-with-icu-format#using-the-trans-component

Upgraded to v13.0.1 (other i18next pkgs were upgraded as well) and the error was gone.

The solution is:

  • create new i18next.d.ts file
  • declare new module and “override” CustomTypeOptionsType
// src/@types/i18next.d.ts

declare module 'i18next' {
	interface CustomTypeOptions {
		allowObjectInHTMLChildren: true
	}
}

This is not yet fixed. The code in the top level comment now (11.16.7) results in:

ERROR 
TS2322: Type '{ children: (string | Element | { count: number; })[]; i18nKey: string; count: number; }' is not assignable to type '{ children?: Record<string, unknown> | ReactNode; components?: readonly ReactNode[] | { readonly [tagName: string]: ReactNode; } | undefined; count?: number | undefined; ... 9 more ...; t?: TFunction<...> | undefined; }'.
  Types of property 'children' are incompatible.
    Type '(string | Element | { count: number; })[]' is not assignable to type 'Record<string, unknown> | ReactNode'.
      Type '(string | Element | { count: number; })[]' is not assignable to type 'Record<string, unknown>'.
        Index signature for type 'string' is missing in type '(string | Element | { count: number; })[]'.

ERROR 
TS2322: Type '{ name: string; }' is not assignable to type 'ReactNode'.
  Object literal may only specify known properties, and 'name' does not exist in type 'ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal'.

The linked PR allowed passing one object as children, but not a list of different objects mixed with strings or nodes. All of these use cases were “supported” via {}, which, as far as I know, accepts a whole lot of types. That’s why using {} is generally discouraged.

For the first error, I think this should work:

type TransChild = React.ReactNode | Record<string, unknown>;

export type TransProps<...> = {
  children?: TransChild | TransChild[];
  ...
};

However, this doesn’t fix the second error. And I’m not sure we can fix it? Here it’s about the children of strong, which is defined in React itself.

Upgraded to v13.0.1 (other i18next pkgs were upgraded as well) and the error was gone.

Doesnt work man, error is still there

I personally don’t use objects in HTML tags in my code, so I can’t really say anything about it. But it sounds like such a toggle might be the best way forward. 👍

Sorry for the delay guys! We can change the TransProps to also accept array of object and ReactNode, but that doesn’t solve the problem entirely, HTML element children are no longer accepting objects, which means this will not work:

<Trans i18nKey="userMessagesUnread" count={count}>
  Hello <strong>{{name}}</strong>
</Trans>

So strong element doesn’t “accept” objects anymore.

That’s not something that we can’t control in “theory”… We can always use type augmentation to change the HTMLAttributes children prop to also accept objects. The only problem is that all HTML elements will start accepting objects again, not only the HTML elements wrapped within Trans component.

Anyway, I created a draft PR https://github.com/i18next/react-i18next/pull/1492 that solves it. If you accept the condition above, we can merge it. I want you to hear your thoughts first!

need @pedrodurek’s feedback here 🙏