typescriptreturn-type

Base function return type on type of local variable


Is there a way to set the return type of the function to depend on the type of a local variable?

I have a set of validation methods:

validateA(things: Thing[]): { badA: string[], badB: number[] }
validateB(things: Thing[]): { anotherBadA: string[], anotherBadB: number[] }

And then I have a wrapper meant to just essentially run all of them and merge their results:

validateAllTheThings(things: Thing[]) {
    const problems = {
      ...this.validateA(things),
      ...this.validateB(things),
    };

    return problems;
}

So, validateAllTheThings should have the return type that's the same as the type of problems, which is all the returned objects merged together.

The cleanest way I've found it to use ReturnType:

function validateAllTheThings(things: Thing[]): ReturnType<WrapperClass['validateA']> & ReturnType<WrapperClass['validateB']> {

But this is super verbose, and won't scale well. It would be nice if I could just tie the function's return type to inferred type of problems.


Solution

  • Generally speaking, most use cases for call signature return types are met by either:

    It sounds like you're looking for a way to get some or all of the dynamic behavior of type inference while also annotating the return type explicitly. It's not clear to me what use case that would serve.


    Still, I'd say that if you're looking for a way to say "combine the return types of the following methods via intersection" in a less cumbersome way, you could write a utility type to do that:

    type MergeReturns<
        T extends Record<K, (...args: any) => any>,
        K extends keyof T
    > = { [P in K]: (x: ReturnType<T[P]>) => void } extends
        Record<K, (x: infer I) => void> ? I : never;
    

    This type just uses ReturnType and combines them using a technique similar to that shown in Transform union type to intersection type. And then you can use it:

    class WrapperClass {
        validateA(things: Thing[]): { badA: string[], badB: number[] } { ⋯ }
        validateB(things: Thing[]): { anotherBadA: string[], anotherBadB: number[] } { ⋯ }
        validateAllTheThings(things: Thing[]):
            MergeReturns<WrapperClass, "validateA" | "validateB"> {
            const problems = {
                ...this.validateA(things),
                ...this.validateB(things),
            };
            return problems;
        }
    }
    

    and you can verify that the returned type is equivalent to the desired type:

    type Z = MergeReturns<WrapperClass, "validateA" | "validateB">;
    /* type Z = {
        badA: string[];
        badB: number[];
    } & {
        anotherBadA: string[];
        anotherBadB: number[];
    } */
    

    Finally, taking the question at face value, you could just annotate the return type using typeof of the local variable:

      validateAllTheThings(things: Thing[]): typeof problems {
          const problems = {
              ...this.validateA(things),
              ...this.validateB(things),
          };
          return problems;
      }
    

    But this only works if you're not emitting declaration files with the --declaration compiler option. Otherwise you'll get the warning that the declared return type refers to an inaccessible name:

    validateAllTheThings(things: Thing[]): typeof problems { // error!
    //                                            ~~~~~~~~
    // Return type of public method from exported class 
    // has or is using private name 'problems'.
        const problems = {
            ...this.validateA(things),
            ...this.validateB(things),
        };
        return problems;
    }
    

    So those are the options I can see. Personally I'd rather either annotate or allow inference and not try to split the difference, but there might be use cases where one of the alternatives is preferable.

    Playground link to code