Luau Type Checking: Optional table fields preventing casting if defined in the value

Reproduction Steps
The following code will show this issue:

type example1 = {
	a: boolean;
	b: boolean?;
}
type example2 = {
	a: boolean;
	b: boolean;
}

local t = {}
t.example1_invalid = {a = false, b = true} :: example1
t.example1_valid = {a = false} :: example1
t.example2_valid = {a = false, b = true} :: example2

Expected Behavior
I would expect the cast to be considered valid by the type checker.

Actual Behavior
The cast (t.example1_invalid in the provided reproduction code) gives an error:

W000: Type 'example1' could not be converted into '{| a: boolean, b: boolean |}'

Workaround
Create a new variable of the casted type and set it without explicitly casting:

local example1_workaround: example1 = {a = false, b = true}

Issue Area: Studio
Issue Type: Other
Impact: Moderate
Frequency: Rarely
Date First Experienced: 2021-08-28 18:08:00 (+12:00)

This isn’t a problem with tables, but that the :: casting operator doesn’t allow casting to less specific types (with an exception for any). For instance, boolean? can be converted into boolean using ::, but not vice versa. For an expression A::B, B must be able to be converted into typeof(A), and not all boolean? values are valid for boolean, which makes it generate a warning.

--!strict
local a:boolean = true
local b:boolean? = true
local c:boolean? = a::boolean? -- warning
-- W000: (4,20) Type 'boolean?' could not be converted into 'boolean'
local d:boolean = b::boolean

This also applies to table types.

--!strict
type val = {a:number}
type opt = {a:number?}
local a:val = {a=1}
local b:opt = {a=1}
local c:opt = a::opt -- warning
local d:val = b::val

So the reason why this is invalid is that example1 ({a:boolean,b:boolean?}) is less specific than {a:boolean,b:boolean}.

1 Like

Ok, that makes sense. Though it seems strange to me since in the invalid A :: B?, the value A is a valid construction of type B? so it should be able to cast. Shouldn’t the “direction” of casting be going from A → B (instead of B → A) in which case this wouldn’t be a problem?

If it did it the other way, then casting would only work with a less specific type. If A is of type number?, then the expression A::number would be invalid because not all number? are valid number. A better way to think about it is :: being a type refinement operator, the type must be a subset of the type of the value (or be the any type). The purpose of :: is to refine the type to something more specific, rather than to make it less specific. Considering this, it makes sensed to see if B can be converted to typeof(A). Conversions to less specific types are done implicitly (like passing a number to a function which expects a number?).

An example of the purpose of :::

--!strict
local function digit_warn(s)
	return string.byte(s)-string.byte"0"
	-- these generate warnings, because the return type is number?
end
local function digit_ok(s)
	return string.byte(s)::number-string.byte"0"::number
	-- ::number refines the type to number, which stops the warnings
end

If casting to a less specific type is intended, then casting to any first will make it work ((Value::any)::less_specific_type).

1 Like

Alright, thanks for your explanation.

So I guess the issue I’m running into is that there is no way to explicitly cast to a less specific type. An operator for that would be a useful addition, I think. Main use cases are avoiding creating locals (casting inline), setting table keys, and overriding variables, since you can’t implicitly cast in those situations.

This disables type checking entirely, so it is not a good solution imo :confused: The whole point of using types is so that I can get type checking.

There wouldn’t have to be a new operator, but making both cases work so that A::B works if typeof(A) can be converted to B, or if B can be converted into typeof(A). I believe this proposal is intended to do that.

I’m not sure what you mean by overriding variables.

1 Like

Yeah that proposal looks about right, I think.

Sorry, overriding variables I included as a bit of a throwaway idea - probably shouldn’t have in hindsight, although it is interesting to think about. I meant essentially changing the type of a variable when you give it a value, i.e.:

local example: string;
example: boolean  = true

Although I don’t this specifically is a good idea (I don’t know if Luau would even be able to handle this if it was allowed, and this can be handled better by creating a new variable), I included it because it’s conceptually similar to setting values in a table:

local t = {}
t.example: boolean = true

This is not valid because you can’t set the type explicitly on something that is already defined (a variable) or with an index into a table. This is a problem if you have a more complex type that you want to get type checking for when you define it, such as with the the code from the OP:

type example1 = {a: boolean, b: boolean?}
local t = {}
t.example1_invalid = {a = true, b = true} :: example1

Which is one of the reasons why I’m wanting an explicit cast, since the implicit cast is unavailable there.