Search code examples
reactjstypescripti18nextreact-i18next

How to type a string that should match an i18next translation key that matches a string and not a nested object?


I want to declare i18next translation keys as property values in an object and type it as a Record<string, SomeType> in a way that it only accepts values that are valid i18next translation keys (only leafs as I don't want it to accept keys for sections that have nested keys so that the result is always a string when I pass the key to t()).

I have this code (using i18next 22.4 and react-i18next 12.2)

import { useTranslation } from 'react-i18next';
import type { TFuncKey } from 'i18next';

// A map of paths to translation keys
// I want TS to complain here if I try to use a string that
// doesn't match a translation key matching a string.
// This below works otherwise but `TFuncKey` is a bit too loose type as
// it also accepts e.g. 'Navigation` which results in an object, not a string.
const pageTitles: Record<string, TFuncKey> = {
  '/': 'Navigation.home', // note: plain string values here, not calling `t()`
  '/about': 'Navigation.about',
  '/other': 'Navigation.other',
};


function useTitle(path: string): string {
  const { t } = useTranslation();
  return pageTitles[path] ? t(pageTitles[path]) : t('ErrorMessages.error');
  // The previous line currently gives a TS error because t(pageTitles[path]) may
  // return an object (not matching the return type of string in this method)
}

I can workaround the error by using t(pageTitles[path]) as string but then it might break at runtime as the TFuncKey type is a bit too loose and I can accidentally pass a value like ''Navigation'' which won't result in a string when passed to t().

If I have the map object inlined like this then the code works as intended:

function useTitle(path: string): string {
  const { t } = useTranslation();
  return {
    '/': t('Navigation.home'), // note: calling `t()` here directly
    '/about': t('Navigation.about'),
    '/other': t('Navigation.other'),
  }[path] ?? t('ErrorMessages.error');
}

but I'd like to refactor the map to be declared elsewhere (outside this React hook) where I can't use useTranslation() as my actual code is a bit more complex (and has more routes) than the example here. So if possible I'd like to have it more like in the first example if it's possible to fix the typings so that TS is satisfied with it.

I'm not sure if I can use the generics of TFuncKey or some other type from i18next to solve my issue.


Solution

  • This problem will be solved just by upgrading to i18next 23.x.

    The type TFuncKey will need to be replaced with ParseKeys as mentioned in the migration guide (the type was renamed).

    I was originally using using i18next 22.4 (and react-i18next 12.2) but after upgrading to i18next 23.4 (and react-i18next 13.0) the code above works just as intended as it will complain if trying to use for example 'Navigation' as a value in the pageTitles object.

    This is due to the new redesigned types in i18next 23 and the returnObjects option which seems to globally default to false (may also be set explicitly as an option per call of t()).