Description
🔎 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
💻 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.