Home > front end >  Generic mapping of return type in Typescript
Generic mapping of return type in Typescript

Time:11-04

Let's assume we have the following object:

    interface X {
      a: () => number;
      b: number;
    }

And the following type:

    type MethodsOf<T> = {
      [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
    }[keyof T];

The MethodsOf type will narrow only the keys of an object whose values are functions. For example:

    type A = MethodsOf<X>; // "a"
    const a: A = 'a';
    const b: A = 'b'; // Error: Type '"b"' is not assignable to type '"a"'.

Now I am trying to create a generic type that will receive an X interface but will return a type whose keys are narrowed to MethodsOf<X> and its values are the types of the return values of the MethodsOf<X>. Example:

    type ValuesMapping<T> = ???
    
    // No error:
    const mapping: ValuesMapping<X> = {
      a: 3
    }
    
    // Error:
    const mapping: ValuesMapping<X> = {
      a: 'blarg'
    }

I was trying to implement type ValuesMapping<T> as follows:

    type ValuesMapping<T> = {
      [K in MethodsOf<T>]: ReturnType<T[K}>;
    };

But I am getting the following error:

    TS2344: Type 'T[K]' does not satisfy the constraint '(...args: any) => any'.   Type 'T[MethodsOf<T>]' is not assignable to type '(...args: any) => any'.     Type 'T[T[keyof T] extends (...args: any[]) => any ? keyof T : never]' is not assignable to type '(...args: any) => any'.       Type 'T[keyof T]' is not assignable to type '(...args: any) => any'.         Type 'T[string] | T[number] | T[symbol]' is not assignable to type '(...args: any) => any'.           Type 'T[string]' is not assignable to type '(...args: any) => any'.

For some context of the issue, I am trying to create a generic mocking type:

type MethodsOf<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

type MockMapping<T> = {
  [K in MethodsOf<T>]: Mock<ReturnType<T[K]>>;
};

// usage
class X {
  a() {
    return 1;
  }
  b = 2;
}

const x: MockMapping<X> = {
  a: jest.fn(() => 42),
};

Any help would be appreciated!

CodePudding user response:

TypeScript is unable to understand that the keys K of T calculated by MethodsOf<T> only point to valid function properties. The only thing TypeScript is sure is that K is actually a key of T. That's why indexing is still possible.

So we need to help TypeScript understand what the T[K] should be by using another conditional.

type ValuesMapping<T> = {
  [K in MethodsOf<T>]: 
    ReturnType<T[K] extends (...args: any[]) => any ? T[K] : never>;
};

This compiles without an error.


Playground

  • Related