Luau typechecker won't widen an array of a given type to a more generic type

Reproduction Steps

Copy/paste this source code into a script into a blank place.

Source Code
--!strict

local partA = Instance.new("Part");
local partB = Instance.new("WedgePart");

--Bums me out that this list can't derive BasePart as the most specific common type among these two
--variables, but it's not that big of a deal in the grand scheme of things.
local parts = {partA :: BasePart, partB};

--We can convert type Part -> Instance
local partAsInstance: Instance = partA;
--And even BasePart -> Instance
partAsInstance = parts[1];

--But we CANNOT convert {Part} -> {Instance}
local partsAsInstances: {Instance} = parts;

Expected Behavior

I expect the cast to succeed, as BasePart is more specific than Instance, and likewise, {BasePart} is more specific than {Instance}

Actual Behavior

I get a typechecking error.

Workaround

As always, any-casting.

Issue Area: Studio
Issue Type: Other
Impact: Moderate
Frequency: Sometimes

2 Likes

Actually, this might be an even more obvious demo:

Hi! This is intentional behavior. Widening an array like this is actually unsound, and can lead to type safety bugs. In technical terms, even though BasePart is a subtype of Instance, {BasePart} is not a subtype of {Instance}. Arrays (tables with an indexer, to use Luau terminology) are invariant, meaning they do not have a subtyping relationship with other arrays. This is because if they had subtyping relationships (they were covariant), you could write code like this:

local x: {BasePart} = {}
local y: {Instance} = x -- Widen x to {Instance}
local z = Instance.new("Decal")
table.insert(y, z)
print(x[1].ClassName) -- Decal
-- We have now inserted a Decal into an array that is supposed to only
-- contain BaseParts!

Ahh, good point. I forgot about how these two variables will share the same exact mutable list.

I was curious so I tested with a frozen table, but the type checker doesn’t seem to track that. No big deal, that might be asking too much.

There might be better language to use in the warning to make this clearer? “Type ‘BasePart’ could not be converted into ‘Instance’” makes me think the Luau type checker is barfing, since BasePart can be converted into Instance. How about “casting mutable tables is unsound”?