2

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

aditya
  • 1,978
  • 3
  • 17
  • 22

2 Answers2

3

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.

  • 1
    Thank you, I'm choosing this as the answer because of the links to discussions that actually answer my question as to why overloading is the only way to solve this and not conditional return types. – aditya Feb 24 '21 at 06:02
  • @aditya - NP, I remember sinking quite some time into asking myself the same question before stumbling upon the discussion. An unfortunate design limitation and looks like something that will be reconsidered in the long run ( after all, the inference *does* work here ) – Oleg Valter is with Ukraine Feb 24 '21 at 10:37
2

I added overloadings and Demo<boolean> as a return type.

In this particular case T extends boolean practically the same as Demo<boolean>.

But please keep in mind, generics are tricky. extends does not mean equal.

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


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

const out = func(true);

Playground link