Skip to content

not tracking potential state change in object literal with literal type property #61761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
manuelbarzi opened this issue May 25, 2025 · 1 comment

Comments

@manuelbarzi
Copy link

🔎 Search Terms

TS2367, no overlap, object property mutation, method call, type narrowing, union type

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about TS2367 and common bugs.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.0-dev.20250525#code/PQKgUASgogigqgSWgWSgOQCoGUwFoAEAzgC4CGxApgFz4AeAngDT4D2ATgJYUB2ZxHLbvgAUaRlEZZGAdQCUefACMKAC1IA3AWxoAzdgHdSbACaNFpAMYBrQycacA5iuKMANhR3El9IpQAOhGAgwGBgxPR+FPgQLIosXgC8+ADeYPjpdDTcAK4AtspsaRn0WXkFRensXLzkAtw0AERoDfgAPvgNUC3tDVjdHdINoRn4emy2xsKyNOosHMYVSpY2RpPT+LPzi47OUzNzCyPunnsbB2AAvqEWgiT4bLHxNDFxiSmLtDQADIyLJfg-RZVHh8OqNLq-RZjCZTd4jEaEfQcYgWFQiYgqDiEAB0wJq-EEsjh8JJFlIhCinQaVEWJJJGKx2No+CSDJxzIA1PgAIxfWl0kaKNgUUhWfmk8mUvo0gUCtnYnyszE4nxc3ni2VCkVi2UZMkUgbUjVy5VMln4eXMgjq3UkrWi416yUdZoy20jeWKi2mnzWvnuwXCh0Cq4jC6QwXLGFE1ICxHI1Ho0140GE4m6-WUrpugPexnMpX5-B+x12oM622ZjrS0vwz3m+sl3Ppe0VjPOhqDHMBy0N02cnn+5ut2tVppG5t5lV9xmqwe1wPajWhjLh4YZY7EWGxunxlFo4TylO1NM79sGqnd91Hzggk9CJLjhcZEfusc1yc36qph8dLrPlty1HDsuwAqdcVvfE6nNXohknV9Kw7V0wK-O8CV-Ts4OHICQ0WNdtg4JwtxjDU90TQ9k0gn8SLfDtsxQyjv3vGC+jAhDzylCdm1QqDBBgwY2JwxCL1Az9GLQ6DH2aQSl1oi9kLExlj3QmD-3goSSRXdIriuMAbm4QgWHcbEOG4PRhAaDAoCwDB7keYgGnkPTbiMigTLMlgLIAYQAQSwKA7NeQh8FyFh1AoYLoVWVhuCiEgKD8RzQjPQL4jNJIh3SB5XgVc1MtS4gIKYlTH3-bZ7OxKKTCmdd0n0wzjPJCk2C3bK0oLBIkl5ZgGjarxmSxQckpGerXOxJqKBa4Q+tyzqMp6vr8B8QavmGvUXMawhmtairlMkzq-waBb7NYKjmMGrp5F00bjNM8yGl8-yCuC0LwuC8xrAmGK4v8JKwBSmaCwBcqcq9fKZr2vjSqGEG0o+lZqqckaNrciapsBlkDtwbqOkWgbgux1b5GRgyxrRnbQcx+bcZO5bgqJxYbtRrbJoptLIYfA6qWO15TuK6CLqS66Ufc+7HoCvrguIbI2AM-BN2+3wEr+gGKqB8GKrB2HCo51SYZGGbNxqxmRfJ6a1apgEefiOh8BWta6tNln0c1y2fhp3m6YBB38CZ8bnbZnWzpKrnmmtrxdcG5oruuEW7s8h6-Il+ypZluWdgj2KlcSpzVZy9Xtdm4GDd24PJMO2qCuxDPjZJhrme28387d8Pbft4n1tJzbG5mxUDvd3rabt+mfb9s2IbLqGub6VvI+CvoY+cru3Pj7yk+ei00+CjPFfivxRgeXJ8DQC0WHwS7ksLguS8pjLC91qT9YyGaa6Rzv6-9nuLbmq2PZt-HvYd0dsvT+rMm5pT7tTQenth6AJNiA8epd+ZT2hrPSeQhBaLz9qvROT1FqvQiqfW2vIvj4FINwYwS18AAFZSHkMoTwYwwUkQYj5hJPi0dL433anlQuWtuFB2QRhMqIwxgiHcF4DgeUADcdt8AAB5ByyI4ByDkQD4QzSqmsSuhsPDEUrmI4QEi5EZWUQomhyjVHqIEdieG0ZK5jwDuAwqHUupfAHnjWBJDR5Oy-rfA6tDW5e1oT4hBTiJ5CMtuONBkSo5CyAA

💻 Code

/*
REQUIREMENTS
- state: xy, orientation (N,E,S,W)
- behavior: forward,backward,right,left by steps
*/

type Robot = {
    x: number
    y: number
    orientation: "N" | "E" | "S" | "W"

    forward(): void
    backward(): void
    right(): void
    left(): void
}

const robot: Robot = {
    x: 0,
    y: 0,
    orientation: "E",

    forward() {
        switch (this.orientation) {
            case "E":
                this.x = this.x + 10
                break
            case "S":
                this.y = this.y + 10
                break
            case "W":
                this.x = this.x - 10
                break
            case "N":
                this.y = this.y - 10
                break
        }
    },

    backward() {
        switch (this.orientation) {
            case "E":
                this.x = this.x - 10
                break
            case "S":
                this.y = this.y - 10
                break
            case "W":
                this.x = this.x + 10
                break
            case "N":
                this.y = this.y + 10
                break
        }
    },

    left() {
        switch (this.orientation) {
            case "E":
                this.orientation = "N"
                break
            case "S":
                this.orientation = "E"
                break
            case "W":
                this.orientation = "S"
                break
            case "N":
                this.orientation = "W"
                break
        }
    },

    right() {
        switch (this.orientation) {
            case "E":
                this.orientation = "S"
                break
            case "S":
                this.orientation = "W"
                break
            case "W":
                this.orientation = "N"
                break
            case "N":
                this.orientation = "E"
                break
        }
    }
}

console.info("TEST robot")

console.info("CASE robots moves forward one step")

{
    robot.x = 0
    robot.y = 0
    robot.orientation = "E"

    robot.forward()

    console.assert(robot.x === 10, "robot x is 10")
    console.assert(robot.y === 0, "robot y is 0")
    console.assert(robot.orientation === "E", "robot orientation is E")
}

console.info("CASE robots moves backward one step")

{
    robot.x = 0
    robot.y = 0
    robot.orientation = "E"

    robot.backward()

    console.assert(robot.x === -10, "robot x is -10")
    console.assert(robot.y === 0, "robot y is 0")
    console.assert(robot.orientation === "E", "robot orientation is E")
}

console.info("CASE robots turns left one step")

{
    robot.x = 0
    robot.y = 0
    robot.orientation = "E"

    robot.left()

    console.assert(robot.x === 0, "robot x is 0")
    console.assert(robot.y === 0, "robot y is 0")
    console.assert(robot.orientation === "N", "robot orientation is N")
}

console.info("CASE robots turns right one step")

{
    robot.x = 0
    robot.y = 0
    robot.orientation = "E"

    robot.right()

    console.assert(robot.x === 0, "robot x is 0")
    console.assert(robot.y === 0, "robot y is 0")
    console.assert(robot.orientation === "S", "robot orientation is S")
}

console.info("CASE robots turns right one step from N to E")

{
    robot.x = 0
    robot.y = 0
    robot.orientation = "N"

    robot.right()

    console.assert(robot.x === 0, "robot x is 0")
    console.assert(robot.y === 0, "robot y is 0")
    console.assert(robot.orientation === "E", "robot orientation is E")
}

console.info("CASE robot moves to x 100 and y 50 and ends with orientation N")

{
    robot.x = 0
    robot.y = 0
    robot.orientation = "E"

    for (let i = 0; i < 10; i++)
        robot.forward()

    robot.left()

    for (let i = 0; i < 5; i++)
        robot.backward()

    console.assert(robot.x === 100, "robot x is 100")
    console.assert(robot.y === 50, "robot y is 50")
    console.assert(robot.orientation === "N", "robot orientation is N")
}

🙁 Actual behavior

TypeScript throws TS2367: This comparison appears to be unintentional because the types '"E"' and '"N"' have no overlap. after the robot.left() call. This suggests TypeScript incorrectly narrows robot.orientation to the literal type "E" after the assignment robot.orientation = "E", ignoring the mutation in left().

Same applies for the robot.right() method.

🙂 Expected behavior

TypeScript should recognize that robot.orientation can be modified by the left() method to any value in the union type "E" | "S" | "W" | "N". The comparison robot.orientation === "N" should not trigger a TS2367 error, as the left() method can set robot.orientation to "N" when starting from "E".

Same applies for the right() method.

Additional information about the issue

This issue resembles #55215 (), where TypeScript fails to account for class field mutations in methods, and #51586 (), where mutations in async contexts (e.g., setTimeout) are not tracked. In my case, the mutation occurs in a method of a standalone object, not a class or async context, but the core issue seems similar: TypeScript does not track property mutations in method calls.

The error prevents valid test cases from compiling, even though they work correctly at runtime.
A workaround is to use a type assertion (e.g., (robot.orientation as "E" | "S" | "W" | "N") === "N") or a helper function, but this is cumbersome and shouldn’t be necessary for correct code.

@MartinJohns
Copy link
Contributor

Another duplicate of #9998.

@manuelbarzi manuelbarzi changed the title not tracking potential state change in object literal with union type property not tracking potential state change in object literal with literal type property May 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants