microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.06k stars 12.29k forks source link

Conditional on property not propagated #59125

Open Sainan opened 2 days ago

Sainan commented 2 days ago

🔎 Search Terms

property undefined propagation

🕗 Version & Regression Information

Not a regression as far as I can tell.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.0-dev.20240702#code/JYOwLgpgTgZghgYwgAgJICUIHc5QCbIDeAUMmcmAJ4AOEAXMgM5hSgDmA3KedVAPYAjOAOAAbYFQD8DEAFcAtgOhdyyKLgmVpTFuy4BfYsVCRYiFBhBtMjWaLBFuZKrQbNWVlT35CR4qjIKSlAGRgh8IMxq2Lh4AAp8fKIAKnw2dmAJScgAvMgAFFAx+IwMGMV4ANoAugCUZehW6fY1uQB8jqoA9F1oeBBwoqKUADQUABbAjMhYfHYEs1AA1nROyD1FYLJQINE4JQB0MGKm+QAe7chnB7yCwmKayACEOXmyIP3HIBB4tV5kPWQACFZGAxhAAG4QXagCYod78IY-ZAIOCMCBjCTIUDhKBFBBgYbIaD8KClNbhSIOaiJUQNJoQWwtaq5ZA1f7IGB8KAFSlRIr7Ah8GB7WKMWqdVTkYAiwoVG4+e7+SjPV7Id6fUA-CUkKV65A0pI3WSMcZywV-NaqQzWtabba7Q2iAxcIA

💻 Code

interface IReward {
    type: string;
    probability?: number;
    rarity?: string;
}

interface IRngResult {
    type: string;
    probability: number;
}

const rewardPoolToResultPool = (rewards: IReward[]): IRngResult[] => {
    // Ideally, this would work:
    //return rewards.filter(x => x.probability !== undefined);
    // But, even in the unrolled case, it incorrectly errors:
    const pool: IRngResult[] = [];
    for (const reward of rewards) {
        if (reward.probability !== undefined) {
            pool.push(reward);
        }
    }
    return pool;
};

🙁 Actual behavior

It complains that the probability property may be undefined, even tho it was explicitly checked for that.

🙂 Expected behavior

It accepts the push as valid.

Additional information about the issue

No response

yaKsirhC commented 2 days ago

You can convert the type on your own pool.push(reward as IRngResult);

Sainan commented 2 days ago

I know that casting is an option, but it's generally more desirable to prove than promise something to a compiler.

MartinJohns commented 2 days ago

Duplicate of #42384.

Sainan commented 2 days ago

I don't think these are quite the same issue, since in that case it's about inferring which variant of a union type is active in the given branch, whereas in this case, there is no union type, only an object and its properties.

MartinJohns commented 2 days ago

It's about narrowing a property propagating to the parent type. You expect by checking the property probability that your IRewad type suddenly is compatible with IRngResult, but that's not how narrowing works. Checking the property does not change the parent type.

Sainan commented 2 days ago

Inheritance is involved nowhere here. Please don't speak if you have nothing of value to add.

MartinJohns commented 2 days ago

I did not mention inheritance anywhere. Please don't say I have nothing of value to add when you don't understand the problem at hand. 🙃

Checking a property like probability can be used to narrow the type of the object (what I referred to as parent) when it's a union type. It does not change the type of the parent from { probability?: number } to { probability: number }, that's not supported.

Quoting @jack-williams:

Type guards do not propagate type narrowings to parent objects. The narrowing is only applied upon access of the narrowed property which is why the destructing function works, but the reference function does not. Narrowing the parent would involve synthesizing new types which would be expensive. More detail in this comment.