Luau Type Checking Release

Ah, gotcha. As you note right now there’s no distinction, and it’s doubtful that one would exist in the future but it might indeed be more clear to use an explicit indexer as an attempt to document that holes are expected.

1 Like

Are there any plans to support the equivalent of .d.ts files which just contain typing information for other libraries?

This would be very useful, as it would let other people type libraries while the maintain catches up.

3 Likes

Second this! I find that, in many cases, it’s just better using untyped luau code at runtime, even though types are helpful for external users of a library.

I find myself abusing the any type when porting existing code (or even new code) around the current flaws of the type system. If we could write regular untyped code and simply add typings on top of it, it would be very helpful.

1 Like

Well, not just holes necessarily. It could also imply that negatives, fractions, zero, and math.huge / -math.huge are expected.

Yes this please! Not only would it help with third party code, but it would help with porting my own code over since I have thousands of lines of code that I can’t reasonably add types to without redesigning it.

I know it’s on the todo list but tables don’t play well with type refinement:

--!nonstrict

local function foo(bar: {any}?)
	local baz: {any}
	if type(bar) == "table" then
		baz = bar
	end
end

Even though this is type safe, it says there’s a type mismatch between baz and bar.

Alright last reply for now since Discourse is yelling at me, but uh… Can you work on making type errors shorter please?

I no joke got a type error that’s 10,768 characters long. It causes like 8 Qt errors when I mouse over it. It feels more than a little excessive, and impossible to debug to boot.

Code here
--!strict

local Queue = {}
Queue.__index = Queue

function Queue.new(init: {any}?): Queue
	local self = {
		_values = {},
		_read = 1, -- where we read from (the back of the queue)
		_write = 1, -- where we write to (the front of the queue)
	}
	if init then
		local len = #init
		local values = table.create(len)
		for i, v in ipairs(init) do
			values[i] = v
		end
		self._values = values
		self._write = len + 1
	end

	setmetatable(self, Queue)
	
	return self
end

function Queue.push(self: Queue, value: any): boolean
	if value == nil then
		return false
	end
	self._values[self._write] = value
	self._write += 1
	
	return true
end

function Queue.pop(self: Queue): any
	local value = self._values[self._read]
	if value == nil then
		return nil
	end
	
	self._values[self._read] = nil
	self._read += 1
	
	return value
end

function Queue.peek(self: Queue): any
	return self._values[self._read]
end

function Queue.count(self: Queue): number
	return self._write - self._read
end

function Queue.iter(self: Queue)
	local values = self._values
	local max = self._write
	local i = self._read - 1
	
	return function()
		i += 1
		if i < max then
			return values[i]
		end
		return nil
	end
end

export type Queue = typeof(Queue.new({}))

return Queue
5 Likes

As strict type checking currently stands, it is unusable!
Without tooltips saying what type varibles and functions are, I have no idea if they were correctly inferred.
When defining class/object types its just one work around after another until one works.
However, I understand most of this is planned so please keep up the good work :slight_smile:

Still I persisted in trying to use strict mode, this time I produced an internal error…

Error: W000: Free types leaked into this module's public interface. This is an internal Luau error; please report it.

Code
--!strict

----- Module Init -----

local InventoryClass = setmetatable({}, {
	__index		= function(_, key) error(tostring(key)..' is not a valid member of Inventory', 2) end,
	__metatable	= function(_) error('The metatable is locked', 2) end
})

local InventoryPrototype = setmetatable({}, {
	__index		= function(_, key) error(tostring(key)..' is not a valid member of Inventory', 2) end,
	__metatable	= function(_) error('The metatable is locked', 2) end
})

InventoryPrototype.__index     = InventoryPrototype
InventoryPrototype.__newindex  = function(_, key) error(tostring(key)..' is not a valid member of Inventory', 2) end
InventoryPrototype.__metatable = function(_) error('The metatable is locked', 2) end

export type Item = {
	
}

type InventoryMembers = {
	ManualOrder: boolean,
	Items: { Item }
}

----- Private Functions -----

----- Public Functions -----

function InventoryClass.New(manualOrder: boolean?)
	local newInventory: InventoryMembers = {
		ManualOrder = false,
		Items = {}
	}
	
	if manualOrder then newInventory.ManualOrder = true end
	return setmetatable(newInventory, InventoryPrototype)
end

-- W000: Free types leaked into this module's public interface. This is an internal Luau error; please report it.
export type Inventory = typeof(InventoryClass.New())

----- Module Return -----
return InventoryClass

Edit: Found a second case which produces this error while investigating another error.
Uncomment export type InventoryTwo to produce internal error.

Error: W000: Type '{| MamualOrder: boolean, Slots: {Stack} |}' could not be converted into 'InventoryMembers' and above internal error

Code
--!strict

----- Module Init -----

local InventoryClass = setmetatable({}, {
	__index		= function(_, key) error(tostring(key)..' is not a valid member of Inventory', 2) end,
	__metatable	= function(_) error('The metatable is locked', 2) end
})

local InventoryPrototype = setmetatable({}, {
	__index		= function(_, key) error(tostring(key)..' is not a valid member of Inventory', 2) end,
	__metatable	= function(_) error('The metatable is locked', 2) end
})

InventoryPrototype.__index     = InventoryPrototype
InventoryPrototype.__newindex  = function(_, key) error(tostring(key)..' is not a valid member of Inventory', 2) end
InventoryPrototype.__metatable = function(_) error('The metatable is locked', 2) end

export type Stack = {
	ItemName: string,
	Amount: number,
	Durabilty: number?,
	Quality: string?
}

type InventoryMembers = {
	ManualOrder: boolean,
	Slots: { Stack }
}

----- Private Functions -----

----- Public Functions -----

function InventoryClass.New(manualOrder: boolean?)
	local newInventory: InventoryMembers = {
		ManualOrder = false,
		Slots = {}
	}

	if manualOrder then newInventory.ManualOrder = true end
	return setmetatable(newInventory, InventoryPrototype)
end

function InventoryClass.Reload(inventory: InventoryMembers)
	return setmetatable(inventory, InventoryPrototype)
end

-- This line does NOT produce any errors
local testVarible: InventoryMembers = { ManualOrder = false, Slots = {} }

-- W000: Type '{| MamualOrder: boolean, Slots: {Stack} |}' could not be converted into 'InventoryMembers'
export type Inventory = typeof(InventoryClass.Reload{ ManualOrder = false, Slots = {} })
-- W000: Free types leaked into this module's public interface. This is an internal Luau error; please report it.
--export type InventoryTwo = typeof(InventoryClass.Reload(testVarible))

----- Module Return -----
return InventoryClass

Edit 2: Found a work around for this issue using only setmetatable:
export type Inventory = typeof(setmetatable({ ManualOrder = false, Slots = {} }, InventoryPrototype))

1 Like

Found another issue with type intersections, it is not possible to index any key of a variable.
This issue does not require strict mode for it to throw an error.
Uses the example that is provided in the documentation.

Code
--!nonstrict

type XCoord = {x: number}
type YCoord = {y: number}
type ZCoord = {z: number}

type Vector2 = XCoord & YCoord
type Vector3 = XCoord & YCoord & ZCoord

local vec2: Vector2 = {x = 1, y = 2}        -- ok
local vec3: Vector3 = {x = 1, y = 2, z = 3} -- ok

-- W000: Type 'XCoord & YCoord & ZCoord' does not have key 'x'
print(vec3.x)
-- W000: Type 'XCoord & YCoord & ZCoord' does not have key 'y'
print(vec3.y)
-- W000: Type 'XCoord & YCoord & ZCoord' does not have key 'z'
print(vec3.z)

Edit: My current work around is to redefine the type in full with intersection applied manually, this of course avoids the use of & and the errors stemming from it.

2 Likes

If someone overrides a type which luaU has defined by default then the errors it produce can be confusing, same for if someone has a variable name that maches a luaU type.
For example:

--!strict
type typ = {
	number:number
}
local number = {}
local test:typ = {
	number = number
}

type typ2 = {
	boolean:boolean
}
type boolean = {}
local test2:typ2 = {
	boolean = true
}

While i doubt anyone would do this on purpose, if someone was to accidently do this it could be confusing.

newproxy() says that it needs 1 argument even though it should be optional
setmetatable() does not support newproxy() as the first argument

1 Like

setmetatable doesn’t support newproxy during runtime though?

Oh yeah, I messed up XD.
I apparently forgot what newproxy was.

With the inability to use typeof without causing internal errors, and not liking the setmetatable work around I found, I continued to look for a type safe way to produce class prototype structures. This post is a warning to always make backups and how luau type checking is still unstable.

This work has permanently corrupted our place without any recent backups, and reverts are ineffective.
I had a module script in ReplicatedFirst named ModuleScript which I was playing with.
Everything was working fine until one change instantly crashed studio.
I am currently trying to recreate this change in a separate place.

Story Details

As normal I opened the recovery file and copied the last commited script to my clipboard.
I opened the original place and found that no other commits were lost so I deleted the recovery file.
I then attempted to open the module script to copy in the recovered version, it crashed when it opened.

The next time I opened studio, I was not presented with an option to recover so proceed straight to the original place.
The place was just as I had it before the crash, but I noticed I had uncommitted changes for ModuleScript.
Thinking this might be the cause of the crash I pressed commit, instant crash.

Again no option for a recovery, but now any attempt to open the place would mean cause a crash.
I thought it was because I had the module open in the editor, so asked my co-dev to open the place and delete the module script.
He was unable to open the place, crashing as well, at this point we attempted to revert the place.

First we reverted to before the last commit, and the place still crashes on open.
Second we reverted by several days, the place still crashes on open.
Our last option is to load a backup from a month ago.

Before we load our manual backup we want to let roblox staff look at this issue
We can not provide an rbx file as we cant open the place to save it, our place id is 4559239318
Here is a paste bin of the version I had on my clipboard, this one does not crash studio.

Studio crashing for this specific place occurs 100% of the time and it continues to crash even with rolling back the versions. My OS and my other co-developers OS are both Windows 10.

(Please can this be moved to Studio Bugs)

1 Like

Thanks for the report, we’re going to fix this specific crash. Note however that type checking is independent of the Studio version update / autosave / backup mechanism; what’s crashing Studio in your case is just the contents of the problematic script, and rolling back the place to the version before that script was introduced should make it possible to open again.

1 Like

I have now been able to reproduce the crash consistently. Paste the following code into a new module and then uncomment line 42. Studio will then crash. I am running windows 10 and have tested this code on a place which has no other scripts. The reason I believed it was a luau issue is because the problematic code is mostly type definitions and setmetatable calls.

Problematic Code
--!strict
-- Paste into a new module and uncomment line 42 to crash studio

----- Module Init -----
-- Generic module init

local GenericModule = setmetatable({}, {
	__index		= function(_, key) error(tostring(key)..' is not a valid member of Module', 2) end,
	__metatable	= function(_) error('The metatable is locked', 2) end
})

type LockedMetatable<self> = {
	__index: (self, any) -> (),
	__metatable: (self) -> ()
}

----- Prototype Init -----
-- Generic prototype init

local GenericPrototype = setmetatable({}, {
	__index		= function(_, key) error(tostring(key)..' is not a valid member of Generic', 2) end,
	__metatable	= function(_) error('The metatable is locked', 2) end
})

GenericPrototype.__index     = GenericPrototype
GenericPrototype.__newindex  = function(_, key) error(tostring(key)..' is not a valid member of Generic', 2) end
GenericPrototype.__metatable = function(_) error('The metatable is locked', 2) end

type GenericMembers = {
	Name: string,
}

type GenericMethods<self, prototype> = {
	__index: prototype,
	__newindex: (self, any) -> (),
	__metatable: (self) -> (),

	Foo: (self, number) -> ()
}

type GenericPrototype = typeof((function()
	-- local tbl: GenericMethods<Generic, GenericPrototype>
	local mt: LockedMetatable<GenericPrototype>
	return setmetatable(tbl, mt)
end)())

export type Generic = typeof((function()
	local tbl: GenericMembers
	local mt: GenericPrototype
	return setmetatable(tbl, mt)
end)())

----- Prototype Function -----
-- Generic functions for the generic prototype

function GenericModule.NewGeneric(name: string): Generic
	local newGeneric = {
		Name = name
	}

	return setmetatable(newGeneric, GenericPrototype)
end

function GenericPrototype:Foo(x: number)
	return
end

----- Module Return -----

return GenericModule

So I am just now starting to get into more luau stuff and was wondering a few things that I couldn’t figure out based on reading everything in this thread.

The first question is, is the type for the function arguments suppose to limit the argument to that type, returning an error if its not? because right now I am finding it treat args as anything. if you classify them as a string or number and pass through a boolean, it goes through anyway. Or am I mistaking on how these types work.

The second question is, what would the Player’s type be? Would it be player: Player, or player: Instance, or player: Userdata, or player: Object… im not sure

Would be nice to have a list of types and their use cases! :smiley:

I’ve tried several different ways but I can’t seem to figure out how to refine an Instance into a BasePart as typeof returns Instance when applied to a Part. What is the recommended way to refine, say, the Parent property of an Instance to a BasePart to be able to access its Position property in strict mode?

2 Likes

@zeuxcg I was scrolling through the beta list and noticed this entry has no “ⓘ” with linked thread, which means if I was not yet familiar with type-checking from keeping up announcements I would not know what this means:

Maybe this beta entry should link to this topic.

1 Like

I’m wondering too, glad someone else has the same issue :sweat_smile: