Luau Type Checker Incorrectly Handling Types Within IF Statements (Luau Type Checker Beta)

There are two conditions I have noticed where my Luau types are incorrect. The first one is in this example:


The CustomKeyframe type is incorrectly coerced to a boolean, even though the actual part of the check I used was the Broken property of the object… not the object itself

Another case where the type checker failed was in this example scenario:


Here the method’s types function just fine, but when placed in an if statement:

It appears to lose track of the type and casts the method call to unknown

Expected behavior

The if statement should be checking the type of the object and coercing / casting it appropriately given the nature of the clause, as opposed to not understanding method call types nor properties of custom objects. It also should not abstract away custom types, like expanding CustomKeyframe into its full type information, it should just say the CustomKeyframe type for self

1 Like

Additional examples found:


I believe this one, however, is not a related bug and is just due to the type checker not consolidating types by operands (i.e. expecting / type checking T if and only if T supports __le like in C++, as opposed to expecting T to take on all types, i.e. allowing T=Vector3 but not T=Instance resulting in find<Instance>({...}, ...) being an erroneous function call that results in a type error)

Thanks for the report! I am able to tell from some of the screenshots that you included that you are using the New Type Solver Beta, but in the future, it’s helpful to include this information explicitly to ensure that the bug report gets routed correctly. If you feel blocked by these issues in any way, feel free to turn off the beta. These do indeed appear to be actual bugs. The occurrence of a blocked type (like *blocked-443455*) basically anywhere in an error is a dead giveaway that a bug has occurred. We are continuing to investigate and fix bugs in the New Solver Beta, including stuff with constraint solving not completing (the blocked types showing up everywhere) and refinements going awry (likely the cause of your initial bug report). If you’d like to help with this process, submitting code reproductions (rather than screenshots of code) helps us significantly.

The last error you received, about le<T, T>, is basically just a limitation of the type system right now. We have to implement support for bounded polymorphism (sometimes called “generic type constraints” or “bounded generics” or a million other names) in order to be able to express accurately the type of your find function because it does indeed not work on all types T, but rather only on types that can be compared with <=. Since we can’t actually express the bound yet, it creates the error you’re seeing instead.

1 Like

Trimmed Sources:

Binary Search (find)
--!strict

-- ...

local function middle(a: number, b: number): number
	return math.ceil((a + b) / 2 - 0.5)
end

--- Perform the Binary Search algorithm on a <em>sorted</em> array
-- @param array Sorted array
-- @param item Desired item to locate
-- @return Index of the found item, or <code>nil</code> if it doesn't exist in the list
local function find<T>(array: {T}, item: T): number?
	local l, m, r = 1, middle(1, #array), #array
	while l <= r do
		if item <= array[m] then
			if item == array[m] then return m end
			m, r = middle(l, m-1), m-1
		else
			l, m = middle(m+1, r), m+1
		end
	end
	return nil
end


CustomKeyframe (relevant functions)
--!strict
--!native

local CustomKeyframe = {}

do
	local metatable = {}
	local class = {}

	metatable.__index = class
	
	type Rig = Attachment | Bone | Motor6D
	
	type EulerRotation = {
		RotationOrder: Enum.RotationOrder, 
		Rotation: Vector3
	}

	function CustomKeyframe.new(
		object: Rig, 
		pos: Vector3?, 
		rot: (CFrame | EulerRotation)?, 
		tangent: Vector2?,
		broken: boolean?
	)
		return setmetatable(
			{
				Target = object :: Rig?,
				Position = pos or Vector3.zero,
				Rotation = rot or CFrame.identity,
				Tangent = tangent or Vector2.zero,
				Broken = not not broken
			},
			metatable
		)
	end

	-- ...

	-- Return the rotation of the Keyframe as a CFrame
	function class.GetRotation(self: CustomKeyframe): CFrame
		local r = self.Rotation
		if typeof(r)=="CFrame" then return r.Rotation end
		local angles = r.Rotation
		return CFrame.fromEulerAngles(
			angles.X, angles.Y, angles.Z, 
			r.RotationOrder
		)
	end

	-- ...

	-- Quadratic bezier curve
	local function qbez(
		a: number, 
		b: number, 
		c: number, 
		d: number, 
		t: number
	): number
		return (
			t*t*t*(d - 3*c + 3*b - a) 
			+ t*t*(3*c - 6*b + 3*a) 
			+ t*(3*b - 3*a) 
			+ a
		)
	end

	-- Compute a Keyframe interpolation
	function class.Interpolate(
		self: CustomKeyframe, 
		target: CustomKeyframe, 
		t: number
	): CFrame
		if self.Broken then 
			return self:GetRotation() 
		end
		
		t = qbez(0, self.Tangent.Y, 1 - target.Tangent.X, 1, t)
		
		if 
			typeof(self.Rotation) ~= "CFrame" 
			and typeof(target.Rotation) ~= "CFrame" 
			and self.Rotation.RotationOrder == target.Rotation.RotationOrder 
		then
			local vec: Vector3 = self.Rotation.Rotation:Lerp(
				target.Rotation.Rotation, t
			)
			return CFrame.fromEulerAngles(
				vec.X, vec.Y, vec.Z, 
				self.Rotation.RotationOrder
			)
		else
			return self:GetRotation():Lerp(target:GetRotation(), t)
		end
	end

	--- Reset the animation transforms for the rigged target
	function class.ResetTransforms(self: CustomKeyframe)
		if not self.Target then return end
		if self.Target.ClassName == "Attachment" then
			-- You can remove these type casts for testing purposes. It casts `self.Target` as `never`
			(self.Target::Attachment).CFrame = CFrame.identity
		else
			(self.Target::(Bone|Motor6D)).Transform = CFrame.identity
		end
	end

	-- Lock a list of tables
	local function lock(...: {})
		for _, t in {...} do
			table.freeze(t)
		end
	end

	lock(
		metatable, 
		class, 
		CustomKeyframe
	)
end

export type CustomKeyframe = typeof(CustomKeyframe.new(Instance.new "Bone"))
1 Like

Additional cases found for constants:


In if statement, constant is casted to unknown, yet the object variable seems to recognize that this is a VObj, however in the else clause, the type of object becomes unknown, even though it should be able to deduce that it is no longer a VObj


Additional bug found:

--!strict

local fps: number = 0
game:GetService("RunService").PreSimulation:Connect(function(dt: number)
	fps = (fps * 5 + 1/dt) / 6
	-- or just fps = fps
end)

Error seems to persist regardless of how i try to re-word the math

No complicated code here, and not entirely sure where this t1 is coming from

This also happens sometimes