Luau Type Checking Beta!

So I’ve been thinking about the syntax for a while, and I’d like to suggest some updates to it. (keep in mind this is a mixture of multiple different ideas of varying quality, mainly just throwing stuff at the wall to see what sticks)


Type aliases


Current:

type SimpleRotation = {pitch: number, yaw: number}

Proposed:

type SimpleRotation: {pitch: number, yaw: number}

The logic behind this change is that all other areas where types appear use the colon. For consistency, it might be a good idea to add it here too. (the argument could however be made that since this defines a type instead of annotating a type, equals would be more suitable?)


Function types


Current:

type NumberPredicate = (number) => boolean

Proposed:

type NumberPredicate: function(number) => boolean

The logic behind this change is that, in the current system, it’s not completely obvious that function types represent functions. By adding the function keyword, it’d make it a lot more clear that it is, indeed, representing a function.


Union types


Current:

local metadata: string | number = "I am metadata!"

Proposed:

local metadata: string or number = "I am metadata!"

The logic behind this change is that, by using the existing ‘or’ keyword in place of the vertical bar, it makes it more obvious that the type can be either the type on the left, or the type on the right. It would additionally fit better with Lua’s more keyword-oriented syntax.


Any types


Current:

local anyValue: any = nil

Proposed:

local anyValue: any? = nil
local definedValue: any = 2

The logic behind this is that you might want to define a value with any type except nil. It could be productive to make any non-optional by default, and instead define untyped things as any?. Maybe a different keyword would work better for non-optional any here?


Optional types


Current:

local function sample(x: number, y: number?, z: number?)
    ...
end

Proposed:

local function sample(x: number, y: number or nil, z: number or nil)
    ...
end

The logic behind this is that, following the design philosophy of Lua, it might be more appropriate to forgo a dedicated optional syntax and instead reuse union types for this purpose. It’s also way more obvious that a type is optional in this case; question marks are only single characters and can sometimes get lost among the rest of the characters (at least in my experience), which makes the code harder to understand quickly.


Just some random ideas. Feel free to pick any you like and criticise any you don’t :grinning:

14 Likes

https://www.lua.org/source/5.1/lbaselib.c.html#luaB_setmetatable
Unless roblox changed setmetatable (they might have), __meta won’t be set. Most likely a bug with it expecting __meta to be set.

3 Likes

Love this new feature so far, only complaint about it is that it’s kind of unstable and crashes often.

1 Like

Well, this is luau for one thing. And secondly, I have no explanation for why __meta would be expected to be set by setmetatable. It’s very strange especially because it only happens in certain contexts as I showed above.

A few bugs I’ve encountered so far (strict turned off)

image




image


image

1 Like

That last one appears to confirm my suspicion above. It seems like internal features are being “leaked” through type checking. What it looks like to me is that type checking is being done on compiled lua. Could some of the crashes be related to memory corruption of some kind? Very weird stuff.

4 Likes

You can’t confirm or deny that from just looking at my last screenshot, as the bug doesn’t occur when you replace the last line with foo().hello = nil, which is compiled into the same thing

1 Like

The type signature string ? works as in local foo: string ? and opposed to string?. I’m writing a parser for the new syntax, is this intentional?

4 Likes

To mirror some of the more common feedback, I found myself consistently wanting to use a colon instead of an equals sign for type declarations. It felt more natural to write type foo: string than type foo = string.

7 Likes

Types aren’t first class however…

type LinkedList<T> = { head: T, tail: LinkedList<T>? }

works.

4 Likes

We are going to discuss this, but there’s a specific reason for the current syntax:

Consistently, : in the new syntax means “the value to the left has a type specified on the right”

For example,

local a: number

“the value a has a type number”

{ foo: number }

“the table has a key foo which is of type number”

Unlike this, the type alias type X = ... doesn’t say “X is of type …”, it says “X names the type …”, and X isn’t a value - it’s a type name.

4 Likes

Are you referring to the fact that the space can be used between the type and the question mark? If so, yeah, ? is a separate token and so the usual whitespace skipping rules apply.

3 Likes

@zeuxcg Do you think it would be possible to allow for type redefinition or allow types
to work backwards within their scope? Here’s an example of what I might want to do:

type SomeType = {Children: {[number]: OtherType}}
type OtherType = {Parent: SomeType}

Whereas I currently need to do something like this:

type _OtherType<SomeType> = {Parent: SomeType} -- I create an "alias" here so I can use SomeType
type SomeType = {Children: {[number]: _OtherType<SomeType>}} -- I must use _OtherType now and use self referencing to pass this type upwards
type OtherType = _OtherType<SomeType> -- Now I give the type a new alias

If I could redefine types this could become the following:

type OtherType<SomeType> = {Parent: SomeType} -- I create an "alias" here so I can use SomeType
 -- No more underscores looks much less cluttered
type SomeType = {Children: {[number]: OtherType<SomeType>}}
type OtherType = OtherType<SomeType>

Ideally I’d want to be able to do the first one though. That would save a ton of complicated rearranging of types.

4 Likes

Forward declaration would be a good addition, not sure about redefining.

type OtherType -- forward declaration
type SomeType = {Parent:OtherType}
type OtherType = {Children:{[number]:SomeType}}

This would be similar to how you can have two functions communicate with eachother

local f2
local function f1(...)
    --...
    f2()
    --...
end
function f2(...)
    --...
    f1()
    --...
end
3 Likes

I suspect we might want to make the consecutive declarations mutually recursive by default. We were discussing something along these lines but I don’t think this has happened yet…

3 Likes

Just in case somebody is curious, and is bored and there’s a long weekend ahead of us, here’s the full dump of the current type surface that our type checker uses:

Please keep a few things in mind:

  • We are lacking coverage in some areas - not all libraries are fully typed
  • Large parts of this are automatically synthesized from our reflection declarations, but not everything is; there may be gaps in type coverage. You can refer to this if you think our type checker doesn’t understand the types correctly - we may have bugs!
  • There are parts of this that don’t correctly reflect the current syntax, e.g. <Cycle> that you will see in a few places, as well as __meta. This is just an artifact of how the type visualizer prints internal information about types that is not always exposed through syntax in the same way.
  • The document is large! Open with caution :smiley:
6 Likes

Because of the duck typing in roblox-TS, there was an issue where the compiler would always consider any “Instance” to be a “Folder” because technically all instances have the same methods a “Folder” has; this was solved in a hacky way by adding a unique ID to the typings of each instance, but I hope there will a be built-in way of making these types unique from each other, so that “Part” doesn’t relate to “Folder”. It seems right now the type checker doesn’t emit warnings for mismatching instance types.

2 Likes

So it doesn’t support Enums yet! I was curious because it seemed to accept Enum as a type just fine, just no way to specify what type of Enum. I guess I’m glad I wasn’t missing something, though this should probably be rectified before the full launch.

2 Likes

Yeah we currently type all enum entries as EnumItem which is not fully correct as it means that enum items from different enums are compatible from the type system perspective (but aren’t from the runtime perspective).

1 Like

Do ModuleScripts have type checking?

Module:

--!strict
local Module = {}

function Module:Numberify(arg: string) => number
     return tonumber(arg) or 0
end

return Module

Script:

local SomeMod = require(Module)

print(SomeMod:Numberify(123))
-- Output: 123

if I understood the OP correctly, this shouldn’t work as the argument is support to be a string… ?

2 Likes