i18next / react-i18next

Internationalization for react done right. Using the i18next i18n ecosystem.
https://react.i18next.com
MIT License
9.21k stars 1.02k forks source link

TS "excessively deep and possibly infinite" error when creating generic type to specify required namespaces #1290

Closed stezu closed 3 years ago

stezu commented 3 years ago

🐛 Bug Report

I'm receiving an excessively deep and possibly infinite type error in my app even with very few namespaces. In my reproduction I only have two, but locally I have 5 so it doesn't seem like the same issue as what was previously reported. The one thing that I do see as somewhat unique is my attempt to define a generic type which allows me to specify the required namespaces when calling my helper methods.

To be more specific, I have a few helper methods in my app which given a t function and a value, will produce the correct translation for a given enum. They're relied on all throughout the app which uses different namespaces depending on the page/bundle (but the helper methods all use a shared bundle). The goal was to ensure I could type check the helper methods properly and ensure that the consumers were loading the required shared translation namespace.

This generic type supports the following:

const formatDayOfWeek = <T extends TFunction<any>>(
  t: TranslationFunction<'shared', T>,
  day: DayOfWeek
): string => {
  switch (day) {
    case DayOfWeek.Monday:
      return t('shared:monday');
    // ... etc
  }
};

// The dependency exists, so this passes type checking:
const [t] = useTranslation('shared');
formatDayOfWeek(t, DayOfWeek.Monday);

// The dependency exists in a group, so this also passes type checking:
const [t] = useTranslation(['shared', 'accounts']);
formatDayOfWeek(t, DayOfWeek.Monday);

// The dependency doesn't exist, so this fails type checking:
const [t] = useTranslation('accounts');
formatDayOfWeek(t, DayOfWeek.Monday);

// The dependency doesn't exist in a group, so this fails type checking:
const [t] = useTranslation(['users', 'accounts']);
formatDayOfWeek(t, DayOfWeek.Monday);

This part all seems to be working as expected, but I still get the excessively deep error.

To Reproduce

I created a TS Sandbox minimal reproduction of the issue. It uses the latest type definitions for react-i18next and i18next with most of the unnecessary parts removed so it wouldn't be overly complex.

Expected behavior

The typescript error about the types being excessively deep and possibly infinite shouldn't be happening. It's certainly possible that the generic type I produced to be able to specify namespace dependencies of a method is incorrect, but I don't think my use case is unique. At the very least, there may be an opportunity to improve documentation or export a type which users of this library can rely on to support the use case.

Thanks for taking a look, I'd appreciate any help!

Your Environment

pedrodurek commented 3 years ago

Hey @stezu, the excessively deep and possibly infinite error is expected, since you're passing any to TFunction, which will enter in an infinite loop by the moment we call the t function. Take a look at this example: https://tsplay.dev/wQAJjw. We must pass a valid generic to infer the proper keys and return type.

I'm afraid it's not possible to do what you want...

Anyway, I simplified your types a bit here: https://tsplay.dev/N5EkdN And this is the best I could get: https://tsplay.dev/ND5gjm

Let me know in case you get closer, good luck! 😄

pedrodurek commented 3 years ago

I'll close this issue for now, since it's not necessarily a bug, but we can reopen it if needed.

kitsunekyo commented 1 year ago

@pedrodurek i just stumbled over this, since we have the same usecase. we have some shared "maps" where we map certain values to translations. i wanted to pass t into the function, so that i get the correct translation for the current language.

ie

export const getAbsenceLabels = (t: (key: string) => string) => ({
  BAD_WEATHER: t('attendence.badWeather'),
  SICKNESS: t('attendence.sickness'),
  ABSENCE: t('attendence.absence'),
  LEAVE: t('attendence.leave'),
  OTHER: t('attendence.other'),
});

i could use i18next.t directly, but then i lose the reactive language context. and i also dont want to just return the keys as string, as this would break static analysis with i18next-scanner.

is there any recommendation you could give on how to handle such shared maps?