Luau Type Checking Release

Try this:

function run(data: test)
	local func = data.func
	if func then
		func()
	end
end

It’s also more efficient.

3 Likes

Although this is a valid workaround, I’d argue that the type checker should still have transient refining of types throughout the flow of a block of code or expression, even if it’s an indexed property of that value.

As a general rule, if the type system makes you re-write how your code functions at runtime even though your code is theoretically safe and valid, it’s the fault of the type system.

6 Likes

Is there a way I can accept only two or three types into a function? like

function test(x: number or boolean)

end

Not sure about the use case of this, just wondering if its possible, and how to do it if so.

function test(x: number | boolean)

end
3 Likes

Ah i see, is that similar to javascript’s || operator?
Also, thank you.

That’s fair. The assumption is whether it’s allowed for __index to have side effects within the type system.

1 Like

@zeuxcg
Looks like nonstrict mode lets unknown globals slip through the cracks in certain cases, whereas nocheck mode will mark this as a warning:

Emits a warning:

--!nonstrict

local x = {}

function x:y()
	self.value = thisVariableDoesNotExist
end

image

Emits a warning:

--!nocheck

local x: any = {}

function x:y()
	self.value = thisVariableDoesNotExist
end

Emits no warning (even though the exact same code in nocheck mode will emit a warning):

--!nonstrict

local x: any = {}

function x:y()
	self.value = thisVariableDoesNotExist
end

the “any” type annotation matters here because a lot of functions in nonstrict mode can return a table that is inferred as any in nonstrict mode, including the results of table.create(...).

2 Likes

This is great feature for me because I don’t have to use bunch of assert command. Is strict mode enabled by default?

There is merit to what you’re saying - I especially take note that external tools would rely on viewing changes in the source to determine if it is strict or not. It feels really hacky to use a comment notation to declare the script as strict typescript… especially for a fully released feature.

I think the best compromise that wouldn’t include changing the LuaSourceContainer API would be to have some non-commented code that declares the script as strict, instead of a comment. For example:

useStrictTypeChecking()

where a useStrictTypeChecking function is declared in the function environment.

Problem with this is that it’ll still introduce new API, only in a more tucked away place. Ultimately, with new features, new interfaces are needed to interact with them. I think adding one new enum for an entirely different coding style would be proper for something like this. I think the main thing though is that when you convert a script to strict typescript, it needs to feel like you’re actually changing something, rather than just adding a typical comment at the top.

1 Like

The following script results in the warning W000: (2,1) Expected to return 0 values, but 1 is returned here

--!strict
return

But the script below results in no warning

--!strict
return 0

Are there any types that are implicitly nullable, or that will be implicitly nullable?

local part = Instance.new("Part")
part = nil

This raises a warning because the part variable is not of type Part?, but in my opinion, like objects in other languages, Instance should be implicitly nullable.

Another one that is mildly irritating is references to tables. In all of my custom classes I offer a Dispose method that detaches everything from its containing type (by setting it to nil) so that GC can pick it up. For instance:

type MyType = {
    Var: {[any]: any}
}

function Dispose(obj: MyType)
    obj.Var = nil
end

This will raise a warning since the table is not nullable.

2 Likes

Luau beginning to look like C#, I like this.

The answer is: Yes.

I’ve tried rejoining my game (in studio) but I crashed everytime.

Is it not possible to have mixed type ordered tuples?
A tuple is a fixed length ordered array, I often use them when storing two or three related values.
Here is an example of all the syntaxes I tried and the errors they produce:

--!strict

-- Syntax error: Expected ':' when parsing table field, got ','
type ConnectionArgument = { string, string, boolean }
-- Syntax error: Expected type, got '1'
type ConnectionArgument = { 1: string, 2: string, 3: boolean }
-- Syntax error: Expected type, got '1'
type ConnectionArgument = { [1]: string, [2]: string, [3]: boolean }
-- W000: Type 'boolean' could not be converted into 'string'
type ConnectionArgument = typeof({ '', '', false })

-- W000: Type 'boolean' could not be converted into 'string'
local argument: ConnectionArgument = { 'number', 'ArgOne', false }
-- This is how I would later use the tuple, all three values always used together
local class, name, optional = unpack(argument)
3 Likes

An interesting problem with this is that the index signature syntax (i.e. {[string]: any}) directly conflicts with the syntax of using literal types as keys in an interface. Would {[1]: any} mean that key ‘1’ must have a value of “any” type, or does it mean that all keys must have a 1 type, and all values have an any type?

Granted, literal types aren’t supported yet, but I’d imagine that they should be down the road.

Found a second issue, using assert will result in type refinement as expected.
But when I assert a property to be nil it will now not allow anything to be assigned to that property.
I commonly use this to catch errors for methods which should only be called once per instance.

A third issue is that there is no syntax which allows vararg function types.
I would like ConnectionHandler to be a vararg which always has the first argument as type Player.
Using ... in the type define will result in an error, and so will trying to add extra args to handler

Here is some example code and the error produced:
Network.AssertClass is a custom function which raises an error if param one is not of type param two.

-- If ', ...' is added: Syntax error: Expected type, got '...'
export type ConnectionHandler = (Player) -> ()

export type Connection = {
	--[[ Unrelated fields omited ]]
	Callback: ConnectionHandler?
}

--- Add a callback to this connection, called when a client calls fetch
function Connection:Register(callback: ConnectionHandler)
	assert(self.Callback == nil, 'Connection already has a callback registered')
	Network.AssertClass(callback, 'function', 'Argument #1 to Register')
	--W000: Type '(Player) -> ()' could not be converted into 'nil'
	self.Callback = callback
end

-- Example handler that might be passed to Connection:Register
-- W000: Type '(Player, any, any) -> ()' could not be converted into '(Player) -> ()'
local handler: ConnectionHandler = function(player: Player, argOne: any, argTwo: any)
	print(player.UserId)
end
1 Like
local str = "a"
print(
	(
		str == "a" and "a"
		or str == "b" and "b"
		or "z"
	).."c"
)

results in W000: (173,9) expected a string or number, got boolean | string

1 Like

I think there should be a button in Script Analysis that will hide selected type warnings and make them dark grey somewhere in hidden warnings folder and won’t underline them because there is so many problems with this checker that sometimes there is no workaround other than using –!nocheck but that is not the best solution too after all. I think if you could somehow hide them with ability to show them again it would make it a lot better, something like in planes when there is master caution, pilots
click the button and it stops flashing.

Also there are issues with remote events and bindable events where you can’t have more than 1 argument in Fire or FireServer/Client, it will throw warning like this
RobloxBindableEventsTypeWarning

3 Likes

Good stuff, that’s an issue I ran into quite often! Thanks, and keep up the good work! :+1:

@zeuxcg @Apakovtac

It looks like a recent update caused a regression where do end blocks can make output extremely verbose and unreadable in some cases for strict mode:

Simplified Repro:
Here I have defined a decently-sized interface type, and a variable x being assigned to it, except that I have an extraneous field, which should output a warning.

Code:
--!strict

type DefinedType = {
	foo: string,
	fighters: string,
	dave: string,
	pat: string,
	nate: string,
	chris: string,
	taylor: string,
	rami: string
}

local x: DefinedType = {
	extraneousField = 'This is extraneous',
	foo = 'Hello',
	fighters = '',
	dave = '',
	pat = 'string',
	nate = 'string',
	chris = 'string',
	taylor = 'string',
	rami = 'string'
}

The warning here is extremely readable and easy to diagnose the issue:

However, if I scope the last statement in a do end block, it all the sudden makes the output much more verbose and less readable:

Code:
--!strict

type DefinedType = {
	foo: string,
	fighters: string,
	dave: string,
	pat: string,
	nate: string,
	chris: string,
	taylor: string,
	rami: string
}

do
	local x: DefinedType = {
		extraneousField = 'This is extraneous',
		foo = 'Hello',
		fighters = '',
		dave = '',
		pat = 'string',
		nate = 'string',
		chris = 'string',
		taylor = 'string',
		rami = 'string'
	}
end

When working with code that has more complex types, this becomes completely unmanageable and impossible to diagnose the issue aside from trial and error, guessing where things are going wrong; the warning won’t even fit on the screen with my 1440p monitor!

2 Likes