Why does typescript expect 'never' as function argument when retrieving the function type via generics?

The problem is that the compiler does not understand that arg and bar[fn] are correlated. It treats both of them as uncorrelated union types, and thus expects that every combination of union constituents is possible when most combinations are not.

In TypeScript 3.2 you'd've just gotten an error message saying that bar[fn] doesn't have a call signature, since it is a union of functions with different parameters. I doubt that any version of that code worked in TS2.6; certainly the code with Parameters<> wasn't in there since conditional types weren't introduced until TS2.8. I tried to recreate your code in a TS2.6-compatible way like

interface B {
    foo: MyNumberType,
    bar: MyStringType,
    baz:MyBooleanType
}

function test<T extends keyof Bar>(bar: Bar, fn: T) {
    let arg: B[T]=null!
    bar[fn](arg); // error here
}

and tested in TS2.7 but it still gives an error. So I'm going to assume that this code never really worked.

As for the never issue: TypeScript 3.3 introduced support for calling unions of functions by requiring that the parameters be the intersection of the parameters from the union of functions. That is an improvement in some cases, but in your case it wants the parameter to be the intersection of a bunch of distinct string literals, which gets collapsed to never. That's basically the same error as before ("you can't call this") represented in a more confusing way.


The most straightforward way for you to deal with this is to use a type assertion, since you are smarter than the compiler in this case:

function test<T extends keyof Bar>(bar: Bar, fn: T) {
    let arg: Parameters<Bar[T]>[0] = null!; // give it some value
    // assert that bar[fn] takes a union of args and returns a union of returns
    (bar[fn] as (x: typeof arg) => ReturnType<Bar[T]>)(arg); // okay
}

A type assertion is not safe, you this does let you lie to the compiler:

function evilTest<T extends keyof Bar>(bar: Bar, fn: T) {
    // assertion below is lying to the compiler
    (bar[fn] as (x: Parameters<Bar[T]>[0]) => ReturnType<Bar[T]>)("up"); // no error!
}

So you should be careful. There is a way to do a completely type safe version of this, forcing the compiler to do code flow analysis on every possibility:

function manualTest<T extends keyof Bar>(bar: Bar, fn: T): ReturnType<Bar[T]>;
// unions can be narrowed, generics cannot
// see https://github.com/Microsoft/TypeScript/issues/13995
function manualTest(bar: Bar, fn: keyof Bar) {
    switch (fn) {
        case 'foo': {
            let arg: Parameters<Bar[typeof fn]>[0] = null!
            return bar[fn](arg);
        }
        case 'bar': {
            let arg: Parameters<Bar[typeof fn]>[0] = null!
            return bar[fn](arg);
        }
        case 'baz': {
            let arg: Parameters<Bar[typeof fn]>[0] = null!
            return bar[fn](arg);
        }
        default:
            return assertUnreachable(fn);
    }
}

But that is so brittle (requires code changes if you add methods to Bar) and repetitive (identical clauses over and over) that I usually prefer the type assertion above.

Okay, hope that helps; good luck!

Tags:

Typescript