Variables are type "never" in an if statement

I’ve noticed that a lot of things dont autocomplete in my if statements and after checking, many things are type “never” for some reason.
There is a temporary fix to it but it can get very annoying;
instead of doing object.value == something, create a variable and compare that to x
if path.object.value == x thenlocal y = path.object.value if y == x then


Visual Aids:
type_never
type_workspace


System info
CPU: Intel(R) Core™ i3-7130U CPU @ 2.70GHz
Memory: 8,0 GB
GPU: Intel(R) HD Graphics 620


Enabled Beta Features:

  • Dragger QoL Improvements
  • New Luau type solver
  • Unified Lighting

Reproduction Steps:

  1. Insert a script into workspace
  2. Paste this into the script;
if script.Parent.Name == "Workspace" then
	local A = script.Parent
	--A is type 'never'
	print('#1') --shouldnt print (?)
end

local X = script.Parent.Name
if X == "Workspace" then
	local B = script.Parent
	--B is type 'Workspace'
	print('#2') --should print
end
  1. write ‘A’ and ‘B’ in the corresponding places, and their types are there
  2. playtest: print() in the first if statement prints even tho - from my understanding - it shouldnt

Expected behavior

Variables to be the correct type (not ‘never’)

1 Like

When you directly compare something like script.Parent.Name == "Workspace" inside a condition, the type checker can’t always correctly infer the type of script.Parent within that block. That’s why it defaults to never—basically saying “I have no idea what this is supposed to be.”

  • Direct comparisons confuse the type system – When you use script.Parent.Name directly inside an if, the solver has trouble associating that with a concrete object type.
  • Assigning to a variable first gives the type checker more context – If you first store script.Parent.Name in a variable (X), Luau can resolve the type better because it processes the assignment independently of the conditional logic.

You can try using intermediary variables for property comparisons

local X = script.Parent.Name
if X == "Workspace" then
    local B = script.Parent -- gets proper type
end

also avoid complex property access directly in conditions
Break it into steps and assign to variables when you can.

For now, just know you’re not doing anything wrong—it’s the current tooling being a little too picky. Hopefully they roll out improvements soon.

1 Like

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 Instances 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 :tm: 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.”

5 Likes