Use generic key in return type

Viewed 240

I'm struggling to use a generic key in the return type for a function

With the following object as the "data model" here

const TRANSLATIONS = {
    application_card: { a: string; b: string}
    home: { c: string}
}

I have defined the following function which will take in a key TNamespace from TRANSLATIONS, and return typeof TRANSLATIONS[TNamespace]

function useI18Namespace<TNamespace extends keyof typeof TRANSLATIONS>(
  namespace: TNamespace
): typeof TRANSLATIONS[TNamespace] {
  const { t } = useTranslation(namespace as string);

  switch (namespace) {
    case 'application_card': {
      return {
        a: t('a', TRANSLATIONS.application_card.a),
        b: t('b', TRANSLATIONS.application_card.b)
      }
    }
    case 'home': {
      return {
        c: t('c', TRANSLATION.home.c)
      };
    }
  }
}

However this doesn't "narrow" the type correctly,

I get an error telling me that c is missing from my return value (when returning application_card

And that a and b are missing when returning home

But when I hardcode the return type:

typeof TRANSLATIONS['application_card']

It narrows correctly, and I don't get any errors (in the return for application_card)

It seems that typeof TRANSLATIONS[TNamespace] is just the same as typeof TRANSLATIONS which I don't understand.

Is this a bug in TS? Or am I doing something wrong?

2 Answers

Basically the function is returning a union type, where the exact variant returned depends on the generic parameter, but this dependency isn't kept track of by the type system. There are several ways to approach the problem, but I think the easiest solution would be to ditch the typeof TRANSLATIONS[TNamespace] return type, and use an overload for each namespace:

function useI18Namespace(n : 'application_card'): typeof TRANSLATIONS['application_card']
function useI18Namespace(n : 'home'): typeof TRANSLATIONS['home']
function useI18Namespace<TNamespace extends keyof typeof TRANSLATIONS>(
  namespace: TNamespace
) {
  ..
}

If you use the overloaded function, the return types will be as desired:

const t1 = useI18Namespace('application_card') // inferred type: {a: string, b: string}
const t2 = useI18Namespace('home') // inferred type: {c: string}
const t3 = useI18Namespace('nope') // ERROR: No overload matches this call. ..

TypeScript playground

Actually, your code works fine all that you need to cast before you return the value :

type o = typeof TRANSLATIONS;
type key = keyof o;

function useI18Namespace<TNamespace extends  key>(
  ns: TNamespace
): o[TNamespace] {

  switch (ns) {
    case 'application_card': {
      return {
        a:"asas",
        b: "asas"
      } as o[TNamespace]; // or cast to any
    }
    case 'home': {
      return {
        c: "t('c', TRANSLATIONS.home.c)"
      } as o[TNamespace]; // or cast to any
    }
  }
 return {} as any;
}

Type problem is typescript is not aware what is the exact value of TNamespace while defining your function so it infers the return type as an intersection of properties like this: { a: string; b: string} & { c: string}. But in runtime TNamespace is known and it can narrow down to selected property type. Here you need to convince typescript that return types are matched with the function signature. You can do it by casting it to the corresponding type or the any type.

Playground

Related