Luau Type Checking Beta!

Hi everyone!

The new version of Studio brings a pair of little improvements to Luau:

First, we should be much smarter about doing type inference on groups of functions that call one another. Code like the following should work fine now:

--!strict

local Foo = {}
Foo.__index = Foo

function Foo.new()
	local self = setmetatable({}, Foo)	
	self.fooConn = function()
		-- Previously, we would warn that we can't find Foo:method
		-- OK now.
		self:method() 
	end

	return self
end

function Foo:method()
	print("foo")
end

local foo = Foo.new()
foo.fooConn()

Secondly, we’ve fixed a bug where long method call chains would require inordinate amounts of CPU and RAM to typecheck.

local s = ("hello"):lower():lower():lower() -- If you did this a few dozen times, Studio started having a bad time.
6 Likes

Is this gonna be enforced on all code, Are we gonna have to use new syntax?

1 Like

Luau is and will always be an additional tool that you can choose to use if you want to.

You can even freely mix typed and untyped code in the same game. This will always be the case.

3 Likes

I’ve just enabled initial support for require tracing in Luau. Here’s how it works:

Luau can trace through a require() call if it looks like one of the following:

local M = require(workspace.This.Whatever.CoolThing)
local N = require(script.Parent.Something.Useful)

If Luau sees something like this, it will ascribe the proper type to M or N above.

Secondly, all type aliases defined in a required script are available. Just qualify them with the name of your import alias:

-- Lib
--!strict

local exports = {}

local CoolThing = {}
CoolThing.__index = CoolThing
function CoolThing.new() return setmetatable({}, CoolThing) end
function CoolThing:method() print('Hello World!') end

-- Define a type name for our class
type CoolThing = typeof(CoolThing.new())

exports.CoolThing = CoolThing

return exports
-- SomethingElse

--!strict

local Lib = require(script.Parent.Lib)

local t: Lib.CoolThing = Lib.CoolThing.new()
--        ^ We have to name the script that the type comes from
t:blah()   -- error: CoolThing has no method :blah()
t:method() -- OK!

Something that doesn’t work yet, but is in the works is tracing requires through calls to game:GetService. Stay tuned!

6 Likes

I’m impressed! This is starting to look more and more like the kind of Typed Lua I’ve been anticipating. The typeof operator in particular is going to be very useful for many things, let alone exporting types between modules.

There is one problem with this method of exporting types between modules though. Since types are static information, there are many cases in which you would want to use types from another module without necessarily calling require on that module. One example I’ve already mentioned before on this thread is in OOP code where a parent class instantiates a child class, and the child class needs the type (but not the value) of the parent class; in this case, it’s hard to avoid a require deadlock unless you manually type each class, and put these typings in a separate module from the class implementation (similar to the role of header files in C++)

Is it possible you could do statements like type MyClass = typeof(require(path.to.MyClass))? This would solve the problem quite elegantly, and make it easier to avoid require deadlocks.


Also, this feature seems to be broken for me right now


It seems to not recognize a require statement referencing an absolute path to a module that clearly exists (even though autocomplete works just fine)

As for relative paths, I can’t seem to get the local t: Lib.CoolThing syntax to work.

EDIT: It also looks like the most recent update doesn’t emit any type mismatch warnings in nonstrict mode (allowing statements like local x: string = 2). Is this a bug?

1 Like

I honestly hadn’t thought about typeof(require(X).Y). It may already work as a side effect of the way the inference engine handles typeof(). As for as the type checker is concerned, typeof(X) is the same work as X.

Both of the things you’ve pointed out look like bugs to me. In particular, requires that are rooted at game don’t seem to work the way they should. We’ll take care of it.

2 Likes

Unfortunately, I had to roll the require() stuff back. It was causing Studio to crash.

Sorry about that! It’ll be back soon!

So now if we want to say that a object was a instance, we can do something like this:

type TypeIns = typeof(Instance)
local Object: TypeIns = Instance.new()

It should work, or not?

@Happywalker, sorry if i only ask it now, but it only return a boolean, the Tenary Operator return other types of values, so it really not help me…

Then you replace “true” and “false” with the values you want to return in those cases.

So for example:

return banned and "You're banned!" or "You're not banned!"
1 Like

@Happywalker, but if i have to compare 3 choices? How i then need to attempt this? What i want to make:

if a == b then
--First
elseif a < b then
--Second
elseif a > b then
--Three
end

I can‘t simply make this:
return a == b and EqualsReturn or GreaterReturn or LowerReturn

But thanks, i really don‘t have know that it was possible to use a Ternary Operator…

You can accomplish that by replacing the ? you would use in an ternary operator with “and” and the : with “or”.

So in your example if you would write it like this in JavaScript using a ternary operator:

return a === b ? First : a < b ? Second : Third

in Lua it would look like this:

return a == b and First or a < b and Second or Third
1 Like

Ok, i will try, thanks for replying! Not fully understand, but i think that i simply need to repeat this thing…

Sorry from annoying, but i have something very strange…
I am trying to make my own class by utilising the new Luau Type and i want to make it possible to make this:

local MyClass: MyClassType = MyClass.new()

and as @fun_enthusiast has say and write, i tried with this. I not am very good with the type keyword, so i tried:

local Spirits = {}
Spirits.__index = Spirits

function Spirits.new(Name:string?, LV1: number?, LV2: number?, LV3: number?, 
LV4:number?, ActualLv:string?)
	--The newSpirit
	local newSpirit = {}
	setmetatable(newSpirit, Spirits)
	
	--The Name
	newSpirit.Name = Name
	
	--The Level
	newSpirit.LV1 = LV1
	newSpirit.LV2 = LV2
	newSpirit.LV3 = LV3
	newSpirit.LV4 = LV4
	
	--The ActualLeve
	newSpirit.ActualLevel = ActualLv or tostring(LV1)

	return newSpirit
end

--Others Things
type Type = typeof(Spirits.new())
Spirits.Type = Type

function getLv(Spirit:Type) => number
	return tonumber(Spirit.ActualLv)
end

function Spirits:CompareSpirits(Spirit1:Type, Spirit2:Type)
	if getLv(Spirit1) < getLv(Spirit2) then
		print("First Choice")
	elseif getLv(Spirit2) > getLv(Spirit1) then
		print("Second Choice")
	elseif getLv(Spirit1) == getLv(Spirit2) then
		print("Equals")
	end
end

return Spirits

but it say me: Unknow symbol ‘Type’, so i tried with this:

local Spirits = {}
Spirits.__index = Spirits

function Spirits.new(Name:string?, LV1: number?, LV2: number?, LV3: number?, 
LV4:number?, ActualLv:string?)
	--The newSpirit
	local newSpirit = {}
	setmetatable(newSpirit, Spirits)
	
	--The Name
	newSpirit.Name = Name
	
	--The Level
	newSpirit.LV1 = LV1
	newSpirit.LV2 = LV2
	newSpirit.LV3 = LV3
	newSpirit.LV4 = LV4
	
	--The ActualLeve
	newSpirit.ActualLevel = ActualLv or tostring(LV1)

	return newSpirit
end

--Others Things
Spirits.ScriptingType = typeof(Spirits.new())

function getLv(Spirit:typeof(Spirits.new())) => number
	return tonumber(Spirit.ActualLv)
end

function Spirits:CompareSpirits(Spirit1:typeof(Spirits.new()), Spirit2:typeof(Spirits.new()))
	if getLv(Spirit1) < getLv(Spirit2) then
		print("First Choice")
	elseif getLv(Spirit2) > getLv(Spirit1) then
		print("Second Choice")
	elseif getLv(Spirit1) == getLv(Spirit2) then
		print("Equals")
	end
end

return Spirits

but it look so, so weird… I not more see the warning, but i not really know if this was the right method, so i am asking here. I know this was in Beta, so i not know if this was my error or a Beta problem.

1 Like

You can do this, but you don’t need to. Luau comes with built-in knowledge of builtin Roblox types.

You can just write

local p: Part = Instance.new('Part')
2 Likes

But @fun_enthusiast, if i want to define my custom part type, then? I am working on a custom class, so i want to make the same only that it work for my class, how to attempt this?

So i not need to make something like this:

local PartType = Instance.new("Part")
function Test(Part:PartType) end

If yes then thanks a lot. I am pretty confused…

I think the thing that’s tripping you up is that types and values are totally separate things.

Values are everything in Lua (not just Luau) that occupies memory by existing. Tables, functions, numbers, and booleans are all examples of values.

Types, by contrast, are basically imaginary. They don’t occupy any memory or do anything when people play your game. They exist only when you are writing your game in Roblox Studio.

local MyTable = {}
--      ^ Creates a name for a *value*

type MyType = {}
--      ^ Creates a name for a *type*

local x = MyType      -- No good because there is no *value* called MyType
local x: MyTable = {} -- No good because there is no *type* called MyTable

So, when you design an OO style object in Lua, you really want to do two related things at the same time:

  1. You want to define a kind of value that can do a bunch of useful stuff, and
  2. You want Luau to know about and track objects that match the shape of those values.

The simplest way to do this is to name the type of the value. This is a distinct step from writing the code to create the value itself. Because they are totally separate, it is easy (and a good idea) to give them the same name.

The way to do this for your own data types is to use the type keyword:

-- First, we create the value MyObject
local MyObject = {}
MyObject.__index = MyObject
function MyObject.new()
    return setmetatable({}, MyObject)
end

-- Second, we create a type that has the same name as the value
type MyObject = typeof(MyObject.new())
--       ^ Type Name      ^ Value name

I hope that helps.

Cheers!

3 Likes

How does this work?

Isn’t that the same as type MyObject = "table"?

I not understand, but else i understand and it helped me out to understand: Thanks!