New type solver can no longer cast to more generic type when strict mode is enabled

Code such as the following provides a typechecking error when the new type solver is enabled:

--!strict

local function foo(_: {[string]: boolean})
	-- Nothing even needs to go in here; just need a function to show off this bug
end

local qux: {baz: boolean} = {baz = false}

foo(qux) --Type '{ baz: boolean }' could not be converted into '{ [string]: boolean }'

This does not produce a typechecking error when the new type solver is disabled.

Expected behavior

I expect to be able to use a more strict type definition in a function which accepts a more generic type as long as both types are compatible; this was not an issue in my scripts which used this functionality previously.

2 Likes

Hi there, thank you for the report! This is actually expected behavior and the types are in fact not compatible with one another because of the behaviors that they actually allow. To understand why, consider this small variation of the program you provided:

local function foo(arg: {[string]: boolean})
	arg["bar"] = true
end

local frob = { baz = false, bar = 42 }
local qux: {baz: boolean} = frob

foo(qux) -- a type error today!
print(frob.bar + 8)

frob has the inferred type { baz: boolean, bar: number }, but we’re allowed to alias it to qux at the type { baz: boolean } because, by width subtyping, it’s okay for us to ignore irrelevant properties of a table. If we allowed the call of foo(qux) without error, bar in frob would have the value true at runtime, but the type number still. This means that we’d accept frob.bar + 8 as well-typed code, but it would throw an error at runtime because you cannot add true to 8. It’s important when thinking about table types (and especially table types with indexers) to remember that they allow writing, and not just reading.

To put it another way (and more concisely), the type { baz: boolean } allows you to read and write boolean values into baz specifically, but doesn’t say anything about the rest of the table, while {[string]: boolean} allows you to write and read boolean from any key. These are not compatible types.