Luau Type Checking Release

10 months ago, we’ve started upon the journey of helping Roblox scripters write robust code by introducing an early beta of type checking. We’ve received a lot of enthusiastic feedback and worked with the community on trying to make sure critical issues are addressed, usability is improved and the type system is ready for prime time.

Today I’m incredibly excited to announce that the first release of Luau type checking is officially released! Thanks a lot to @Apakovtac, @EthicalRobot, @fun_enthusiast, @machinamentum, @mrow_pizza and @zeuxcg!

What is type checking?

When Luau code runs, every value has a certain type at runtime - a kind of value it stores. It could be a number, a string, a table, a Roblox Instance or one of many others. Thing is, some operations work on some types but don’t work on others!

Consider this:

local p = Instance.new("Part")
p.Positio = Vector3.new(1,2,3)

Is this code correct? No - there’s a typo. The way you get to find this typo is by running your code and eventually seeing an error message. Type checker tries to analyze your code before running, by assigning a type to each value based on what we know about how that value was produced, or based on the type you’ve explicitly told us using a new syntax extension, and can produce an error ahead of time:

image

This can require some effort up front, especially if you use strict mode, but it can save you valuable time in the future. It can be especially valuable if you have a large complex code base you need to maintain for years, as is the case with many top Roblox games.

How do I use type checking?

A very important feature of Luau type checking you need to know about is that it has three modes:

  • nocheck, where we don’t type check the script in question.
  • nonstrict, where we type check the script but try to be lenient to allow commonly seen patterns even if they may violate type safety
  • strict, where we try to make sure that every single line of code you write is correct, and every value has a known type.

The modes can be selected per script by writing a comment at the top of the script that starts with --!, e.g. --!strict.

As of this release, the default mode is nocheck. This means by default you actually won’t see the type checking produce feedback on your code! We had to use nocheck by default because we aren’t fully ready to unleash nonstrict mode on unsuspecting users - we need to do a bit more work to make sure that most cases where we tell you that something is wrong are cases where yes, something is actually wrong.

However we highly encourage trying at least non-strict mode on your codebase. You can do this by opting into a different default via a Studio beta:

image

This beta only changes the default mode. Another way to change the mode is to prepend a --! comment to the script - you can do this manually for now, but if anyone in the community wants to release a plugin that does it automatically on selected scripts (+ descendants), that would be swell!

If you really want your code to be rock solid, we recommend trying out strict mode. Strict mode will require you to use type annotations.

What are type annotations and how do I use them?

Glad you asked! (please pretend you did) Type annotations are a way to tell the type checker what the type of a variable is. Consider this code in strict mode:

function add(x, y)
    return x + y
end

Is this code correct? Well, that depends. add(2, 3) will work just fine. add(Vector3.new(1, 2, 3), Vector3.new(4, 5, 6)) will work as well. But add({}, nil) probably isn’t a good idea.

In strict mode, we will insist that the type checker knows the type of all variables, and you’ll need to help the type checker occasionally - by adding types after variable names separated by ::

function add(x: number, y: number)
    return x + y
end

If you want to tell the type checker “assume this value can be anything and I will take responsibility”, you can use any type which will permit any value of any type.

If you want to learn more about the type annotation syntax, you should read this documentation on syntax. We also have a somewhat more complete guide to type checking than this post can provide, that goes into more details on table types, OOP, Roblox classes and enums, interaction with require and other topics - read it if you’re curious!.

What happens when I get a type error?

One concept that’s very important to understand is that right now type errors do not influence whether the code will run or not.

If you have a type error, this means that our type checker thinks your code has a bug, or doesn’t have enough information to prove the code works fine. But if you really want to forge ahead and run the code - you should feel free to do so!

This means that you can gradually convert your code to strict mode by adding type annotations and have the code runnable at all times even if it has type errors.

This also means that it’s safe to publish scripts even if type checker is not fully happy with them - type issues won’t affect script behavior on server/client, they are only displayed in Studio.

Do I have to re-learn Lua now?!?

This is a question we get often! The answer is “no”.

The way the type system is designed is that it’s completely optional, and you can use as many or as few types as you’d like in your code.

In non-strict mode, types are meant as a lightweight helper - if your code is likely wrong, we’re going to tell you about it, and it’s up to you on whether to fix the issue, or even disable the type checker on a given problematic file if you really don’t feel like dealing with this.

In strict mode, types are meant as a power user tool - they will require more time to develop your code, but they will give you a safety net, where changing code will be much less likely to trigger errors at runtime.

Is there a performance difference?

Right now type annotations are ignored by our bytecode compiler; this means that performance of the code you write doesn’t actually depend on whether you use strict, nonstrict or nocheck modes or if you have type annotations.

This is likely going to change! We have plans for using the type information to generate better bytecode in certain cases, and types are going to be instrumental to just-in-time compilation, something that we’re going to invest time into next year as well.

Today, however, there’s no difference - type information is completely elided when the bytecode is built, so there is zero runtime impact one way or another.

What is next for types?

This is the first full release of type checking, but it’s by far the last one. We have a lot more ground to cover. Here’s a few things that we’re excited about that will come next:

  • Making nonstrict mode better to the point where we can enable it as a default for all Roblox scripts

  • Adding several features to make strict mode more powerful/friendly, such as typed variadics, type ascription and better generics support

  • Improving type refinements for type/typeof and nil checks

  • Making it possible to view the type of a variable in Studio

  • Reworking autocomplete to use type information instead of the current system

If you have any feedback on the type system, please don’t hesitate to share it here or in dedicated bug report threads. We’re always happy to fix corner cases that we’ve missed, fix stability issues if they are discovered, improve documentation when it’s not clear or improve error messages when they are hard to understand.

267 Likes

This topic was automatically opened after 18 minutes.

Wow, this is great guys! This would’ve saved me about an hour running code making my latest project. Probably will still help in the future :smiley:

Does this mean this will improve the autofill results to become more relevant?

10 Likes

For integrity purposes will there be an option in the future to allow for the setting to explicitly be set to one mode for all scripts? it would be very beneficial in a group environment. The use case would be helpful for creating more consistent code for a group place in the event of multiple programmers.

If this is possible in the future, would anyone in said group place be able to turn it off or could it be locked somehow to further increase script/game integrity?

9 Likes

Sounds interesting! Getting help with code that might not work automatically is a much needed move.

3 Likes

So to clarify, this means it’s safe to use across all clients, correct? I assume yes but I want to be sure.

Also, what’s the planned syntax for function generics? Something like function<T>(foo: T)?

4 Likes

I’ve seen plenty of updates go towards this and I feel like it’d be a shame to not take use of it. However, I can’t really say that I understand what type checking really is yet…

Is type checking in place to encourage better coding standards which one can and will find in professional work environments outside of Roblox, as well as to allow for better readability?

I’m trying to wrap my head around as to why I, or anybody else should take use of type checking.

3 Likes

I’m really excited for this, especially

—!strict
function testfunc(x: number, y: string, z: any)
—I think this is how it works
end

I’m probably going to use non-strict, because I can be really slow sometimes and easily mess things up.

2 Likes

I sure hope so, would be awesome. I like the updates that the roblox studio team has been putting out. This, the senematic highlights, and temporary tabs.

3 Likes

Yes and mostly yes.

This is intended to help write larger complex codebases that stay robust, as the type checker will find more issues ahead of time. If you’re just experimenting with ideas, you probably don’t need to worry about types - if you’re releasing a game or a library to a lot of users, you might want to use this to make sure your code stays correct.

12 Likes

Congrats on v0! I’m surprised it’s already here!

I have a few issues to report right out of the gate:
The following code gives this warning (This is from a module in my game which I shrunk down to the bare minimum that will show this warning):

--!strict
local ArcadeGameManager = {}

local trackers: {[string]: ArcadeGameStartTracker} = {}

local x = trackers.foo.skipCooldownAureumCost

export type ArcadeGameStartTracker = {
    canStart: ((Player) -> boolean)?,
    skipCooldownAureumCost: number?,
    replayCooldownSeconds: number,
    startGameSync: (Player, any) -> any,
}

return ArcadeGameManager

Secondly, for one required module in particular, I’m getting lots of benign “unknown require” warnings that immediately resolve themselves when I open the module emitting the error. This happens whenever I join my Team Create place (DM me if you need access to the place file).

The module being required is in nonstrict mode, whereas the modules calling require are in strict mode.


The warning on this line immediately disappears when I open the module.

Finally, I was surprised to find out that, in my entire codebase with thousands of modules, only a few warnings remained (granted, it was in a module with strict mode on, whereas most of my code has strict mode off), and some of these were legitimate bugs that only type safety could catch!

This leaves me with two benign warnings in my entire codebase that are problematic, given that I have to either turn off strict mode, or change how my code works at runtime to silence them (it is a micro-optimization of difference in how my code performs at runtime when I try to work around the warning, but it’s still annoying)


It seems like the expression (char ~= nil) and (char:FindFirstChild('HumanoidRootPart')) does NOT refine the type of “char” to be non-nil after the “and” operator. I understand that Improving type refinements for type/typeof and nil checks is on the list, so I hope this gets fixed soon.


My thoughts on strict mode planning to be enabled by default:

This would make it super difficult to manage old codebases, or at least inconvenient to the point where I’ll have to go through every single script and turn non-strict mode on, most likely.

It seems like the more likely scenario, at this point, is for me to write a plugin that automatically turns on nonstrict mode en masse, so that I can turn on strict mode by default and still get nonstrict mode on for all of my old code.

I am not looking forward to trying to finagle all of my old code around the quirks of strict mode; it’s just too much work, and I think it’s much easier to write new code with type safety, and leave old code not typesafe.

8 Likes

I don’t really understand what the : do, could someone explain? :smiley:

Also, the → is like JavaScript => ?

1 Like

Great! now I think I’m going to start using it, I hope it can be used in games already programmed without having to change a lot of things.

But there is a problem, many teams and individuals like me often use Rojo to develop on Roblox, games like Adopt Me and Jailbreak are made that way, it will be very impractical or impossible to use typechecking, we can expect some kind of tool so that we can also use it?

2 Likes

Just to clarify, we do not plan to enable strict mode by default!

We plan to enable non-strict mode by default, which strikes a balance between being permissive and giving the code the benefit of the doubt and finding errors.
Strict mode will forever remain an opt-in feature. In the future we’re going to add ways to mark packages or games as using either mode by default, but regardless it won’t change the fact that by default we’re not planning to use strict mode, ever.

Strict mode is a power-user tool and will remain that way.

Thanks for the other feedback, it’s fantastic - we’ll get right on to this.

15 Likes

Do you have any specific examples of what issues the type checker can find and how necessary it is to take these into account?

In what way can my code not stay correct? Or what do you really mean here?

I appreciate you taking the time to reply! :+1:

Ah, I didn’t realize the distinction between non-strict and nocheck. In that case, this looks like a good compromise in theory. I’d imagine this will come out of beta once false positives are ironed out. It appears as though even non-strict mode is still hard to work around, given that I have thousands of warnings in my codebase when enabling non-strict by default. It also looks like general typings for the roblox API need to be fixed, because I’m getting a lot of false positives that would require me to change the function of my code at runtime to silence:



image
So I hope things get really refined before nonstrict by default is enabled.

It seems like most of the thousands of warnings I get when enabling nonstrict by default are that same “unknown require” bug

3 Likes

I have to ask, does Roblox plan on ever releasing an LSP so we can take advantage of this outside of studio?

8 Likes

Yahoo! Incredibly exited to use this the next time I’m in Roblox Studio. Been waiting on release since it was announced.

Just wondering: for parameters, can types be assumed by the interpreter or are they absolutely required for any type checking to take place?

Here’s an issue I found with nonstrict mode: It looks like when a metatable is typed as “any” (as it would be by default in nonstrict mode) when an __index metamethod is present, it causes certain OOP code to fail that is otherwise benign. This makes it quite a hassle to even have nonstrict mode enabled with pre-typed luau OOP code (which is a pain to convert to strict mode as it is)

I’d say that, in nonstrict mode, either a type would have to be inferred for this metatable that takes into account the __index, or there would have to be a rule in the type checking that, if a table has an “any” typed metatable, a whole host of things would then be possible (which maybe excessively permissive, but I’m not sure what the alternative is for keeping old OOP code warning-free in nonstrict mode).

Also, order of functions defined on a table seem to matter in nonstrict mode.

3 Likes