Search code examples
typescripttypescript-typingstypescript-genericstypescript-types

Typing an overloaded class method


I need to write a wrapper around a method foo that is overloaded by parameter type.

UPDATE: A extends B

UPDATE2: TS playground link

const getFoo: {
   foo(x: number, y?: A): A;
   foo(x: number, y?: B): B;
}

In my TS IntelliSense-enabled IDE, const a = foo(1, {} as A) is of type A, and const b = foo(1, {} as B}) is of type B. So, the overloading is working.

My wrapper function:

const fooWrapper = () => {
  const { foo } = getFoo();
  return {
     modifiedFoo: <T extends A | B>(x: number, y?: T) => {
        // my code
        return foo(x, y);
     }
  }

However, const c = modifiedFoo(1, {} as A) is of type B, instead of A as you would expect.

Trying to add a return type to modifiedFoo:

modifiedFoo: <T extends A | B>(x: number, y?: T):
  T extends A
  ? A
  : T extends B
  ? B
  : never

fixes the return type of modifiedFoo, but it causes a compiler issue in the modifiedFoo definition on the line return foo(x, y), stating

Type 'B' is not assignable to type 'T extends A ? A : T extends B ? B : never'
Type 'A' is not assignable to type 'T extends A ? A : T extends B ? B : never'

Its very odd to me that I can call return foo(x, y as A) to get modifiedFoo to always return A, or return foo(x, y as B) to get modifiedFoo to always return B, but with generics returnFoo(x, y) with y being type T does not return the correct type, unless I add the explicit return type definition which causes the compiler error.


Solution

  • Answer with explanation:

    type OverloadedParams<T> = T extends { (...o: infer U1): any; (...o: infer U2): any } ? U1 | U2 : never;
    
    type OverloadedReturn<T, U> = Extract<
     T extends {
         (...args: infer A1): infer R1;
         (...args: infer A2): infer R2;
     } ? U extends A1 ? [U, R1] : U extends A2 ? [U, R2] : never : never, [U, any]
    >[1];
    
    const gWrapper = () => {
        const { foo } = getG();
        return <T extends OverloadedParams<G["foo"]>>(...args: T): OverloadedReturn<G["foo"], T> => {
            return foo(args[0], args[1]);
        }
    }
    

    Playground link

    The key is getting the parent type of the overloaded argument type (not the subtype) and associating it with the appropriate return type using the definition of the overloaded functions.

    What I still don't understand, and why this took so long to solve, is why

    type OverloadedReturn<T, U> =  T extends {
         (...args: infer A1): infer R1;
         (...args: infer A2): infer R2;
     } ? U extends A1 ? R1 : U extends A2 ? R2 : never : never;
    

    doesn't work.