Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing union types, generics and conditional types causes unexpected "Type is not assignable to type" error

I've hit a problem with type-inference specifically when conditional-types are used within union types.

There may be a shorter way to demonstrate this issue, but I could not find one...

See the problem in action at this playground link.

Consider the following Result<T>, a union-type used to indicate the success or failure of an operation (with an optionally attached value, of type T). For the success-case, I have used the conditional type SuccessResult<T>, which resolves to either OKResult or ValueResult<T> (depending on whether the result should also carry an attached value):

type Result<T = undefined> = SuccessResult<T> | ErrorResult;

interface OKResult {
    type: 'OK';
}
interface ValueResult<T> {
    type: 'OK';
    value: T;
}
interface ErrorResult {
    type: 'Error';
    error: any;
}
type SuccessResult<T = undefined> = T extends undefined ? OKResult : ValueResult<T>;

function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
    return result.type === 'OK';
}

Let's use it with a simple union type:

type C1 = "A1" | "B1";
function makeC1(): C1 { return "A1" }
const c1: C1 = makeC1();
const c1Result: Result<C1> = { type: "OK", value: c1 }; // ALL IS GOOD

Now, instead of the simple union type C1, which is just "A1" | "B1", let use a union type of complex values, C2, in exactly the same way:

type A2 = {
    type: 'A2';
}
type B2 = {
    type: 'B2';
}
type C2 = A2 | B2;
function makeC2(): C2 { return { type: "A2" } }
const c2: C2 = makeC2();
const c2Result: Result<C2> = { type: "OK", value: c2 }; // OH DEAR!

This results in an error:

Type 'C2' is not assignable to type 'B2'.

Type 'A2' is not assignable to type 'B2'.

Types of property 'type' are incompatible.

Type '"A2"' is not assignable to type '"B2"'.

If I remove conditional typing from the equation and define my Result<T> to use ValueResult<T> instead of SuccessResult<T>:

type Result<T = undefined> = ValueResult<T> | ErrorResult;

...everything works again, but I lose the ability to signal valueless success. This would be a sad fallback if I can't get the optional typing to work in this case.

Where did I go wrong? How can I use SuccessResult<T> in the Result<T> union, where T itself is a complex union type?

Playground link

like image 382
spender Avatar asked Sep 19 '20 23:09

spender


2 Answers

type Result<T = undefined> = SuccessResult<T> | ErrorResult;

needs to be

type Result<T = undefined> = SuccessResult<T> | ErrorResult | ValueResult<T>;

then it compiles.

Cheers, Mike

like image 74
MikeJ82 Avatar answered Nov 02 '22 07:11

MikeJ82


Unfortunately specifying the return value of a function is not enough. You need to explicitly return the type. Then it compiles.

This doesn't work

function makeC2(): C2 {
    return {
        type: "A2"
    };
};

This works

function makeC2() {
    const x: C2 = {
        type: "A2"
    };
    return x;
};

Playground link

like image 1
a1300 Avatar answered Nov 02 '22 09:11

a1300