When the compiler compares a type X to see if is assignable to another type Y, it unfortunately cannot spend too much time checking. If they have different apparent structure, the assignment might be rejected even if every valid value of type X is also a valid value of type Y. One such situation where presumably valid assignments get rejected is when you try to assign unions of objects with properties like {a: string} | {a: number} to objects with unions of proprerties like {a: string | number}:
declare let x: { a: string } | { a: number }
declare let y: { a: string | number };
x = y; // error
In order to validate this assignment, the compiler would need to split apart {a: string | number} into {a: string} | {a: number}. While this wouldn't be so bad, the general procedure would be very unwieldy. Imagine the compiler is given {a: string | number | symbol, b: Date | RegExp | string[], c: 1 | 2 | 3 | 4 | 5 | 6} and needs to test against some union of object types. It would need to produce a union of 3×3×6 = 54 members, and similar unions elsewhere would lead to an explosion of types for the compiler to check.
See microsoft/TypeScript#12052 and related issues for more information, and specifically this comment on microsoft/TypeScript#45230 for an authoritative statement about why this can't be done in general.
Before TypeScript 3.5, such assignments always failed. But TypeScript 3.5 introduced support for "smarter" union type checking, as implemented in microsoft/TypeScript#30779. Now, if your union-of-object-types is a discriminated union and some other criteria are met, the assignment can succeed:
declare let x: { a: 0, z: 2 | 3 } | { a: 1, z: 3 | 4 }
declare let y: { a: 0 | 1, z: 3 };
x = y; // okay
This works because the type of x is a discriminated union with a as the discriminant property. The literal types 0 and 1 can be used to determine which member of the union you're in. And so the above assignment works since TypeScript 3.5.
So, hooray.
Unfortunately for you, the union type you have is not a discriminated union. Your would-be discriminant property is not a literal type, but an object type which is itself a discriminated union.
declare let x: { a: { d: 0 }, z: 2 | 3 } | { a: { d: 1 }, z: 3 | 4 }
declare let y: { a: { d: 0 } | { d: 1 }, z: 3 };
x = y; // error
That is, your union type is something like a "nested discriminated union". And TypeScript does not support these. See microsoft/TypeScript#18758 for a feature request asking for such support.
Just to be clear, this is what you're trying to do:
declare let x:
{ result: ResultSuccess, x: number, y: number } |
{ result: ResultFail, x: number, y: number };
declare let y: { result: Result, x: number, y: number };
x = y; // error
And this fails because the result property is not seen as a discriminant of a discriminated union. Oh well.
Your other approach works because you use control flow analysis to narrow what I'm calling y to either the left or the right leg of the union of x, and now there is no union propagation to worry about:
declare let x:
{ result: ResultSuccess, x: number, y: number } |
{ result: ResultFail, x: number, y: number };
declare let y1: { result: ResultSuccess, x: number, y: number };
declare let y2: { result: ResultFail, x: number, y: number };
x = y1; // okay
x = y2; // okay
If you're okay with this approach, and you're willing to sacrifice convenience for type safety, great. Otherwise, since this is just a design limitation of TypeScript, my suggestion here is probably for you to use a type assertion and move on with your life. You know more than the compiler here about the safety of your code, so you can just tell it so:
const buildA = (result: Result) => {
return {
x: 1,
y: 1,
result,
} as A; // assert here
}
Playground link to code