typescriptconditional-typesfunction-signature

Typescript function output cannot be assigned to conditional type


I have a simplified version of a more complex problem. The following causes TSC to throw errors:

type Demo<isTrue> = isTrue extends true ? { a: string } : isTrue extends false ? { b: string } : never;

const func = <T extends boolean>(arg: T): Demo<T> => {
    if (arg) {
        return {a: "hello" };
    } else {
        return { b: "world" };
    }
};

const out = func(true);

Throws the following errors:

    Type '{ a: string; }' is not assignable to type 'Demo<T>'.
    Type '{ b: string; }' is not assignable to type 'Demo<T>'.

The out at the bottom has the correct type on inspection, so just the function definition has the issue. How can I understand this better and how do I solve it?

Playground link


Solution

  • How can I understand this better?

    Take a look at this GitHub thread (also see the original issue). It boils down to the fact that TypeScript does not support the narrowing of function return types when using conditional types. Since the resolution of the Demo type depends on a generic type parameter T, it is the same as if you wrote the conditional directly in the return type annotation.

    The issue should become clearer if we rewrite the Demo type (for demonstration purposes only):

    type D<T extends boolean> = {
        true: {
            a: string
        },
        false: {
            b: string
        }
    }[`${T}`];
    
    const func = <T extends boolean>(arg: T): D<T> => {
        if (arg) {
            return {a: "hello" }; //Type '{ a: string; }' is not assignable to type '{ a: string; } & { b: string; }'
        } else {
            return { b: "world" }; //Type '{ b: string; }' is not assignable to type '{ a: string; } & { b: string; }'
        }
    };
    

    Now it should be crystal clear that D<T> remains unresolved until you provide an argument for the type parameter. That is why const out = func(true); is correctly inferred.

    How do I solve it?

    You are pretty much limited to either using type assertions like as Demo<T> or dropping the generic type parameter and rewriting the signature with overloads as outlined in captain-yossarian's answer.