Type checker cannot assign wider type to table
Consider the following Luau code:
--!strict
type Instance = {__type: "INSTANCE"}
type Model = Instance & { _parent: "Instance" }
local selectedItem: {
instance: Instance
}
local item: Model
-- TypeError: Type
-- '{ instance: Instance & { _parent: "Instance" } }'
-- could not be converted into
-- '{ instance: Instance }';
-- this is because accessing `instance` has the 2nd component of the intersection as `{ _parent: "Instance" }` and accessing `instance` results in `Instance`, and `{ _parent: "Instance" }` is not exactly `Instance`
selectedItem = {
instance = item,
}
This type error raised is even more problematic when you replace Instance and Model types with the Roblox definitions, where a Model is an OOP extension of an Instance.
Is this a bug, or does this need an RFC created to support this behavior? I see it as a bug as a Model is an Instance, but I can see why this may not be desirable in some cases.
This is not a bug if you expect it to work in general, though we can and should allow this particular case because the table is a literal in that assignment. For literals in particular, we can count this amongst a number of known bugs with bidirectional type checking.
The reason it has to be disallowed in general is that if you could pass a table with a property at type Model to something expecting a property at type Instance, the function could assign a non-Model Instance into it, violating type safety when outside the function you have a non-Model at the type Model. It is, by contrast, safe when we know that the table is not aliased because we know that any mutation will not be externally visible.
OK, that makes sense (and hopefully this issue can serve as the case for the non-aliased variant if you aren't aware of another issue describing this problem?). My use case is actually this piece of code:
--!strict
-- Instance and Model are global types in Roblox, where Model inherits Instance
type InstanceWithCleanup = {
instance: Instance,
}
local MagnifyClient = {}
MagnifyClient._selectedItem = nil :: InstanceWithCleanup?
function MagnifyClient._selectItem(item: Instance): ()
if item:IsA("Model") then
-- item is now type Model
-- TypeError: Type
-- '{ instance: Model }'
-- could not be converted into
-- 'InstanceWithCleanup?'
MagnifyClient._selectedItem = {
instance = item,
}
end
end
obviously this passing the type system is desirable, but based on what you are describing this is not possible (maybe another piece of code relies on MagnifyClient._selectedItem.instance to be an Instance, definitely not a Model).
Although maybe I'm not familiar enough with what you describe about literals and perhaps in the Roblox type definitions this would be potentially fixable?
It has nothing to do with Roblox type definitions, but yes, { ... } is a table literal expression. The table does not exist before that point. So, that example also should ultimately be accepted for the same reason.
Looks to be fixed since 0.674 (#1832)