Luau Type Checking Release

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

Another small bug report:

I have a table typed as {[Player]: BasePart?}, since there both can and can not be a BasePart at an arbitrary Player-typed key. Unfortunately, when iterating over this record with pairs, it also sets the value to a type that is nilable, even though it’s impossible to have a nil value with a non-nil key on a table.

This is in a heartbeat-bound polling loop, so even though it’s not too big of a deal to check if proxy then, it’s still a needless truthiness check that I have to place here that affects runtime performance because it thinks proxy can be nil here.

3 Likes

One major grievance I have is the method for importing external types. I have a module right now that requires a module and assigns it to a variable for the sole purpose of using it for type annotations (you can see the type module here). This is annoying, since it has runtime consequences and is a bit unwieldy.

Would it be possible for us to get an import keyword or something like it so that we can import the types from another module/script without having to require it directly?

1 Like

Currently converting my 3D skybox module to use types, but forgot to annotate them on the functions themselves. Got this messed up error which left me confused for a while:

W000: (531,4) Type '({+ AmbientColorDay: Color3, AmbientColorNight: Color3, ... 3 more ... +}, Color3) -> ()' could not be converted into 't1 where Skybox3D = {| AmbientColorDay: Color3, AmbientColorNight: Color3, ... 19 more ... |} ; t1 = (Skybox3D, Color3) -> ()'

Here are the code snippets that caused this:

type Skybox3D = {
	...
	SetViewportFrameZIndex:(Skybox3D,number)->(),
	...

	SetWorldScale:(Skybox3D,number)->(),
	SetCameraOrigin:(Skybox3D,Vector3)->(),

	SetLightSource:(Skybox3D,string)->(),

	SetAmbientColorDay:(Skybox3D,Color3)->(),
	SetAmbientColorNight:(Skybox3D,Color3)->(),
	SetLightColorDay:(Skybox3D,Color3)->(),
	SetLightColorNight:(Skybox3D,Color3)->(),

	SetActive:(Skybox3D,boolean)->(),
	Destroy:(Skybox3D)->(),
}

...

local skySetWorldScale = function(self,scale)
	...
end
	
local skySetCameraOrigin = function(self,ori)
	...
end
	
local skySetViewportFrameZIndex = function(self,zindex)
	...
end

local skySetLightSource = function(self,source)
	...
end

local skySetAmbientColorDay = function(self,ambientday)
	...
end

local skySetAmbientColorNight = function(self,ambientnight)
	...
end

local skySetLightColorDay = function(self,lightday)
	...
end

local skySetLightColorNight = function(self,lightnight)
	...
end


local skySetActive = function(self,active)
	...
end

local skyDestroy = function(self)
	...
end
1 Like

I’m having an interesting issue. As you can probably guess, should the type be nil then number 1 will be used for the assignment.

image

3 Likes

I have no idea how to solve this problem but can we just not have this warning? It seems like it’s going to rarely if ever be useful.

1 Like

It seems types alias statements have some amount of scope to them, so it’s almost always better to feed types annotations forwards. The exception is that you can circularly reference types within another type alias.

I’m not sure if this is intentional behavior for the type system or a bug to be fixed, but I’ve been using the following (extremely verbose, but type safe to external users of the module) boilerplate for typed OOP code:

OOP Boilerplate
--!strict



type ClassName_Members = {
	
}
type ClassName_Methods = {
	
}
type ClassName_Metatable = { __index: ClassName_Methods }

export type ClassName = typeof((function()
	local t: ClassName_Members
	local u: ClassName_Metatable
	return setmetatable(t, u)
end)())

local ClassName = {}

local ClassName_Methods: ClassName_Methods = {
	
}

local ClassName_Metatable: ClassName_Metatable = {
	__index = ClassName_Methods
}

function ClassName.new(): ClassName
	local self = {}
	setmetatable(self, ClassName_Metatable)
	
	-- Name self type to make output less verbose
	type InferredSelf = typeof(self)
	
	return self
end

return ClassName

You can ctrl+f and replace the text ‘ClassName’ with your class’s name
It’s super redundant and verbose, but it does get the job done.

As an example of this in action, I’m currently writing a custom character framework that forks the core “PlayerModule” script with a typesafe style of OOP:

2 Likes

@zeuxcg
Found a small typing issue with the Connect method of RBXScriptSignal

--!strict
local function subscribe(signal: RBXScriptSignal): RBXScriptConnection
	return signal:Connect(function() end)
end

Here’s some incorrect code path parsing.

image

2 Likes

That seems to work, though I’ll be the first one to say “wow I hate this”.

It seems like in order to make it properly typesafe you only have to add types to the metatable though, which seems… more reasonable. Not quite good though.

Thank you for the tip about making internal types though. That helps with debugging a lot.

1 Like