Typescript. Return different objects type depending on enum input argument

112 views Asked by At

I have template names enum and for each enum value function fetchTemplate must return it's own response type.

enum KeyEnum {
  VK = 'VK',
  VIBER = 'VIBER',
}

type VkResponse = { vk: string };
type ViberResponse = { viber: number };

type ReturnTypes = {
  [KeyEnum.VK]: VkResponse;
  [KeyEnum.VIBER]: ViberResponse;
};

I have created function fetchTemplate with this signature:

const fetchTemplate = <T extends KeyEnum>(key: T): ReturnTypes[T]

And with this body :

const fetchTemplate = <T extends KeyEnum>(key: T): ReturnTypes[T] => {
  if (key === KeyEnum.VK) {
    return { vk: 'someString' }; <<- Here i got TS2322 error (description below)
  }

  if (key === KeyEnum.VIBER) {
    return { viber: 1 } as ReturnTypes[T]; <<- No problems, but i dont want use "as"
  }
};

# Unnecessary type definition in the left, just for testing
const vkRes: VkResponse = fetchTemplate(KeyEnum.VK);
const viberRes: ViberResponse = fetchTemplate(KeyEnum.VIBER);

But in the function body i can't just return {vk: 'someString'} or {viber:1}, because i got TS2322:

TS2322:
Type  { vk: string; }  is not assignable to type ReturnTypes[T]
Type  { vk: string; }  is not assignable to type  VkResponse & ViberResponse
Property  viber  is missing in type  { vk: string; }  but required in type ViberResponse 

Why TypeScript can't do type narrowing in this case? Why i need to use "as ReturnTypes[T]", how to avoid it? How i can correctly narrow key type from T to specific .VK or .VIBER and explicitly define certain (not union) return type in IFs blocks?

TypeScript v5.2.2
Node v20.8.0

This one also not working:

  switch (key) {
    case KeyEnum.VIBER:
      return { viber: 1 }; <<- The same TS2322 error
  }
2

There are 2 answers

0
jcalz On BEST ANSWER

Currently control-flow based narrowing (like switch/case or if/else blocks) does not play nicely with generics. The problem is that while checking key === KeyEnum.VK will narrow the type of key, it will not affect the generic type parameter (T in your example). This is considered a missing feature, as requested in microsoft/TypeScript#33014. Until and unless that is implemented, you'll need to work around it.

For now if you want to use a generic function and return an indexed access type like ReturnTypes[T], you'll need to actually perform an indexing operation: that is, get an object of type ReturnTypes, and read a property at the key of type T. Conceptually for your example that looks like:

const fetchTemplate = <T extends KeyEnum>(key: T): ReturnTypes[T] => {
  return {
    [KeyEnum.VK]: { vk: 'someString' },
    [KeyEnum.VIBER]: { viber: 1 }
  }[key]
}; // okay

One possible problem with this is that it requires you to pre-calculate every possible return value. If you call fetchTemplate(KeyEnum.VK), the object will still evaluate {viber: 1} and then throw it away. For a simple case like this that's probably no big deal, but if your code has side-effects or is expensive to compute, then you could refactor to use getters, so that only the relevant code gets run:

const fetchTemplate = <T extends KeyEnum>(key: T): ReturnTypes[T] => {
  return {
    get [KeyEnum.VK]() {
      return { vk: 'someString' };
    },
    get [KeyEnum.VIBER]() {
      return { viber: 1 };
    }
  }[key]
};

Here if you call fetchTemplate(KeyEnum.VK), only the getter for KeyEnum.VK is never run.

Hopefully someday microsoft/TypeScript#33014 will be implemented, since this refactoring, while functional, is potentially confusing.

Playground link to code

4
Yosef Tukachinsky On

What you probably looking for is Function Overloads which in your case will look like this:

enum KeyEnum {
  VK = 'VK',
  VIBER = 'VIBER',
}

type VkResponse = {vk: string};
type ViberResponse = {viber: number};

function fetchTemplate(key: KeyEnum.VIBER): ViberResponse;
function fetchTemplate(key: KeyEnum.VK): VkResponse;
function fetchTemplate(key: KeyEnum) {
  if (key === KeyEnum.VK) {
    return { vk: 'someString' };
  }

  if (key === KeyEnum.VIBER) {
    return { viber: 1 }
  }
}

const vkRes: VkResponse = fetchTemplate(KeyEnum.VK);
const viberRes: ViberResponse = fetchTemplate(KeyEnum.VIBER);