This is a really helpful tutorial and I’m sad I didn’t know of its existence sooner. Lua(u) is the only language I’ve ever programmed in so I’ve missed out on all the goodies (and annoyances?) of other languages. Typechecking has been out for a while but I’ve only ever known how to define the types of arguments and returns of functions, nothing else.
This dumbs down the content really easily since I couldn’t understand the typechecking documentation too well due to my unfamiliarity with types and all. I really wanted to incorporate typechecking for cleaner code, the potential future improvements and to leverage script analysis (especially in VS Code) but since I couldn’t grasp it I ended up shelving it and didn’t feel motivated to revisit it. Now I can start investing more time into applying typechecking in current and future code I write.
Excuse me if I’m wrong since I’m only just learning about type checking, but couldn’t the issue in your example be fixed by adding the ? operator to the type number?
type fun = (string, number?) -> string
if I’m not mistaken, that should allow you to use the types number and nil in place of the function’s second argument.
Yes, that’d fix it. But with that post I made is to show that noting the types in the custom function type didn’t propagate to functions noted with it.
With the new type-on-hover feature, it appears it makes it generic:
(and it infers the return type from the body of the function, instead of the type too)
Is there a way to type check for metatables? Currently, I don’t have a solution on how it works.
These are some ways I tried, to no avail
type met<t, mt> = typeof(
setmetatable(t, mt) -- Unkown global 't', Unkown global 'mt'
)
type met = typeof(
setmetatable(
{},
{__index: typeof(function() end)} -- Unkown global '__index'
)
)
type t = {}
type mt = {
__index: typeof(function() end)
}
local foo: typeof(setmetatable(t, mt)) = setmetatable( -- Unkown global 't', Unkown global 'mt'
{},
{
__index = function()
end,
}
)
type t = {}
type mt = {
__index: typeof(function() end)
}
local foo: t & mt = setmetatable( -- Type 'foo' could not be converted into type 'mt'
{},
{
__index = function()
end,
}
)
type OOPType = {
Print: (OOPType, string) -> nil
-- The type must be declared as first parameter to consider the use of "self"
}
If you don’t write the type inside the function declaration as the first parameter, the linter will throw a warning saying "This function does not take self, did you mean to use ."
Metatables inherently isn’t a value type, they’re just normal tables assigned to another table to give functionality so I don’t think they would add such
although a solution if you really need to
type func = type(function() end)
type metatable = {
__index : func | {any},
__newindex : func : {any},
__call : func,
__tostring : func,
__metatable : string,
__mode : string
}
local function getmetatable(...) : metatable?
return getmetatable(...)
end
can you explain the logical thought process of the computer when you tell it to do something using “::” (assertion) because while i can understand the other type operator on a basic level, the way you explained it using “this works” “this does not work” formula for assertion had me on the ropes
Great tutorial!
I knew a little about type setting but couldn’t figure out how to define types within Tables for a long time, great lesson. Thank you very much!
Only problem I have is how would I get these types globally recognized by server and client scripts?
I found out one of the ways via the Luau documentation (export keyword)
I managed to use this post, coolalex1835’s + 7z99’s reply and the Luau documentation OP linked to construct a rudimentary custom type called “Command” based off of CMDR. It’s a bit confusing to learn this stuff though.
Custom Type - Command
Type Definition Script (ServerScriptService → ModuleScript)
-- Type Definitions
-- Export this type with the Module Script so that we can access it. Not *necessary* but useful just in case
export type executable = (player : Player | nil, args : {string}) -> boolean
export type command = {
Name : string,
HasMetatableAccess : boolean | nil,
Exec : executable,
metatable : {any} | nil
} | nil
-- Type Constructors
local Command = {}
Command.New = function(name : string, metatableAccess : boolean, Exec : executable, metatable : {} | nil) : command
local c : command = {}
c.Name = name
c.HasMetatableAccess = metatableAccess
c.Exec = Exec
c.metatable = metatable
return c
end
return Command
Example Command using the Type Definition:
-- Require the Command Definition
local c = require(script.Parent.Command)
-- Create an Execute Function (that follows the Executable type)
function Execute(p : Player | nil, args : {string} | nil) : boolean
print(args)
return true
end
-- Construct and Return the command
return c.New("test", false, Execute, {})
Third Script to create and execute the command:
local c = require(script.Parent.TestCommand)
c.Exec(nil, {"does", "this", "work", "yet???"}) -- will print the table.
Something else to note which I found really cool is that type checking skills also transfer to the Swift Programming Language that was developed by Apple (doing a uni module with it included). This tutorial has been an invaluable tool that has taken me between several programming languages and helped me throughout my time at university so far.
--!strict
local Value : any = 5
print(Value + 1) -- This will shout at us because we have Value set to any, this means its not sure if this should be possible or not, this is where assertion comes in:
print((Value::number) + 1) -- This will work because we asserted that this would be possible, via setting its type to number.
-- Remember! assertion only applys for that operation if I were to now do
print(Value + 1) -- It would still shout at us