microsoft/typescript-go

Different return type inference when union with common members is reduced

Closed this issue · 1 comments

Steps to reproduce

function validate() {
    if(Math.random() > 0.5) {
        return utilValidate();
    }
    return { valid: true };
};


declare function utilValidate(): {
    valid: boolean;
    msg?: undefined;
} | {
    valid: boolean;
    msg: string;
}

validate().msg; // Error in TSGO

Playground Link

Behavior with typescript@5.8

The return type of validate is { valid: boolean; msg?: undefined; } and validate().msg has no error

Behavior with tsgo

validate().msg raises the error: Property 'msg' does not exist on type '{ valid: boolean; }'. suggesting the return type of validate is { valid: boolean; }. If utilValidate is defined before validate there is no error

Maybe related to #1738

This is a type ordering issue. The type { valid: boolean; msg?: undefined } and the fresh object literal type { valid: boolean } are both strict subtypes of each other, so subtype reduction will keep whichever one is ordered first in a union type. You can force the error with typescript@5.8 by reversing the branches of the if statement:

function validate() {
    if(Math.random() <= 0.5) {
        return { valid: true };
    }
    return utilValidate();
};

declare function utilValidate(): {
    valid: boolean;
    msg?: undefined;
}

validate().msg; // Error in typescript@5.8

Likewise, you can make the error go away in tsgo by moving the declaration of utilValidate before the declaration of validate.

I'll give some thought to whether we can make the strict subtype relation pick a winner here. Ideally the type with the explicitly declared undefined property should be considered a strict supertype of the fresh type without (because the fresh object literal type effectively has an infinite set of properties of type undefined), but it may not be feasible.