Thanks for the report! It is definitely a bug that the type of script.Parent
gets refined to never
in this case. It has nothing to do with defaulting to never
though, and aroloxia is unfortunately mistaken about what never
means. never
is a type that represents impossibility, i.e. it’s a type that cannot be constructed in any way. For a legitimate instance of this, consider the builtin function error
. No matter what you pass to error
, calling the function will never return, so we consider error
to have the return type never
. It’s what is sometimes called a “bottom type” in a type system. In the context of Luau, that means that any operations are permitted on it (with the intuition that it cannot actually be observed that they wouldn’t work since you cannot actually have something at runtime of the type never
).
The underlying cause of the bug here is the type refinement system. When you write script.Parent.Name == "Workspace"
as a condition, the type system creates a refinement that says that the type of script.Parent
is { Name: "Workspace" }
, i.e. a table with a property named Name
that maps to the string literal "Workspace"
. These refinements get intersected with the original type of script.Parent
, so the inferred type of script.Parent
with your conditional block is something like Instance & { Name: "Workspace" }
. Roblox API types like Instance
are all exposed to Luau via a foreign function interface, and so unlike Luau types which are structural (meaning the types define what you can do), types from the API are nominal meaning that they must actually match by name. Because of this, the type system is considering that intersection to be incompatible because Instance
is the type of some foreign thing with a named type, and { Name: "Workspace" }
is the type of a table with at least the property Name
. Since the two types are incompatible, it simplifies that intersection to never
which is what you’re seeing. Binding the property to a local prevents the type refinement from triggering, which is why that intersection doesn’t occur (and therefore the never
type doesn’t occur) in your workaround example.
This whole thing is a tricky issue because there’s essentially two sorts of data things that people are interacting with and really three different sorts of things we need to be able to express in the type system. As far as data goes, we have objects from Roblox like Instance
, Part
, etc. which are all constructed in C++ and exposed to Luau scripts over a foreign-function interface, and we also have any tables that users are constructing in Luau which are native to the language itself. Meanwhile, at the type level, we want to be able to express both the types of things that are definitely those Roblox objects like Instance
, the types of things that are definitely Luau tables, and also separately something more general — the type of “things that allow you to read and write properties at various types” (i.e. something that accepts both tables with some properties and Instance
s with some properties). Internally, we tend to refer to this last class of things as “shape types” since they’re describing the interface to the data, rather than its representation. The refinement system today is producing refinements that are tables, but morally, it should really be producing shapes describing what the test tells us is true of the interface. Likewise, developers probably often mean to describe the shape, rather than the representation, when they write functions that take types like { Name: "Workspace" }
or { x: number, y: number, z: number }
.
If you’ve used the old type solver (i.e. what you get in Studio without the New Type Solver beta feature enabled), you’ll probably note that intersections like this actually work the way you probably would like them to. This is because the old type solver frankly played fast and loose with the distinction between a table type and a shape type, and it’s pretty happy to treat table types as describing those foreign Roblox objects most of the time (though it doesn’t actually do so consistently, and there are cases where you can hit similar problems with it resolving such an intersection to never
). The New Type Solver is a lot more principled about the meaning of types in general (which we believe is desirable generally because it means that programmers will be able to have a more coherent and internally consistent mental model of the type system), but in doing so, it’s unintentionally surfacing gaps in what our type system can accurately describe today.
I honestly can’t tell you exactly what we’ll do to resolve this issue. At a fundamental level, it will be that “the refinement system needs to produce refinements that describe the shape of the data being refined, rather than the representation,” but I don’t know if that will mean exposing to programmers a notion of shape types that then get made by the refinements or if that will mean rewriting the refinement system itself to not rely on constructing that intersection at all (and instead trying to solve for what the two types ought to mean together in the specific context of refinements). This is definitely an issue that we understand well, and absolutely want to rectify. Frankly, it’s one of our few Big Issues
that we’re tracking for the New Type Solver. But unfortunately, it’s one that’s complicated enough that I can’t drop in to just tell you “oh, we’re fixing this next release.”