New Type Solver incorrectly infers 'any' type for table members after table is compared in method

This only occurs with tables that have metatables. The examples use variables within a method, but its also the same for when using methods and seems to also affect complex functions that heavily involve the same operations as in the examples.

--!strict

local Class = {}
Class.__index = Class

type Class = setmetatable<{
	A: number
}, typeof(Class)>

function Class.Foo(x: Class, y: Class)
	local z: Class = setmetatable({
		A = 1
	}, Class)

	if y == z then -- This could also be replaced with an 'assert' function which yields the same bug
		return
	end

	local a = x.A				-- Correctly inferred as 'number'
	local b = y.A				-- Incorrectly inferred as 'any'
	local c = (z :: Class).A	-- Correctly inferred as 'number' but with forced type casting
end

Strangely, if a member of the table has been previously stored before the table comparison, only that member will be correctly inferred after comparison.

type Class = setmetatable<{
	A: {X: number},
	B: string
}, typeof(Class)>

function Class.Foo(self: Class)
	local a = self.A		-- Member previously stored before table comparison
	
	if self == self then
		return
	end
	
	local z = self.B		-- Incorrectly infers as 'any'
	local y = self.A		-- Correctly infers as '{X: number}'
	local x = y.X		-- Correctly infers as 'number'
end

The intellisense shows suggestions when trying to access member tables from the instance itself and fully recognizes them, but when accessing from a stored member variable (e.g. a table)—since they’re inferred as ‘any’ type, —no suggestions will be shown.

type Class = setmetatable<{
	A: {X: number}
}, typeof(Class)>

function Class.Foo(self: Class)
	if self == self then
		return
	end

	local t = self.A.	-- Shows that 'X' exists within the suggestions

	local v = self.A		-- 'A' is Stored as type 'any'
	local u = v.		-- No suggestions shown
end

This has been a constant problem within a huge modulescript I’m working on which heavily utilizes native code generation and I’m forced into using type casts to make the type solver forcefully recognize the types of each member variables, but I thought that was messy and I figured excessive typecasting—especially the ones within loops—could slow down performance when trying to write performance-critical code.

Thank you for the bug report! Agreed, that seems incorrect, and we’ll try to keep you in the loop as we figure out the root cause and fix here.

Type assertions (the X :: Y) syntax should not be expensive. I believe outside of native code generation it should do nothing, and at best inside of native code generation we may use types as hints, but I don’t think that applies to type assertions, only annotations on functions such as Z in:

local function X(y: Z) end
1 Like

To add onto this, there is no runtime cost whatsoever to typecasting. The compiler completely elides the cast at runtime. So, there’s no concern about doing it in a loop, even with native code generation enabled.

2 Likes

Hello again! We’ve just rolled out a fix for this issue. In the snippets you provided we should now be correctly inferring member types, including providing autocomplete suggestions where appropriate, for example:

--!strict

local Class = {}
Class.__index = Class

type Class = setmetatable<{ A: number }, typeof(Class)>

function Class.Foo(x: Class, y: Class)
	local z: Class = setmetatable({ A = 1 }, Class)

	if y == z then
		return
	end
	-- All three locals are inferred to be of type `number`
	local a = x.A
	local b = y.A
	local c = z.A
end

… and …

--!strict

local Class = {}
Class.__index = Class

type Class = setmetatable<{ A: {X: number} }, typeof(Class)>

function Class.Foo(self: Class)
	if self == self then
		return
	end

	local t = self.A
	-- `v` now has type { X: number }
	local v = self.A
	-- intellisense displays `X` as an option after the `|`
	local u = v.|
end

Please feel free to reply on this ticket, or open another ticket, if you run into issues!