Search code examples
typescripttypescript-generics

Enforce return type of method referenced by its name


I have a function that takes as parameters a method name as a string and the object that contains that method. I can easily check that the class of the object contains the method name. But I also want to make sure that it is callable and the return type is of a certain type. How can I do that?


type expected = {
    a: string;
}

function handle<T>(method: keyof T, handler : T ) {
    const f = handler[method];
    return f(); // this does not compile
}


class Handler {
    foo() {
        return 'bar';
    }
    baz() {
        return { a : 'bar'}
    }
}

handle('foo', new Handler()); // compile error - not the expected return type
handle('baz', new Handler()); // should compile

Solution

  • You can solve this by using constrained generic type parameters. Given the type Expected

    type Expected = { a: string };
    

    …you can create an additional generic type parameter K for the key (method name) and constrain it by string. Then constrain T by "a type that has a property at K whose value is a 0-arity function that returns the type Expected". You can annotate this in a concise way using the type utility Record<Keys, Type> like this: Record<K, () => Expected>. Finally, annotate the return type of the function using the type Expected. All together, the description above looks like this:

    TS Playground

    type Expected = { a: string };
    
    function handle<
      K extends PropertyKey,
      T extends Record<K, () => Expected>,
    >(method: K, handler: T): Expected {
      const f = handler[method];
      return f();
    }
    
    class Handler {
      foo() {
        return "bar";
      }
      baz() {
        return { a: "bar" };
      }
    }
    
    handle("foo", new Handler()); /* Error
                  ~~~~~~~~~~~~~
    Argument of type 'Handler' is not assignable to parameter of type 'Record<"foo", () => Expected>'.
      The types returned by 'foo()' are incompatible between these types.
        Type 'string' is not assignable to type 'Expected'.(2345) */
    
    handle("baz", new Handler()); // Ok