(
Currently unfinished, but can still be read. Feedback is appreciated.)
Hello! Today, I’m here to teach you the basics on how to use the New Type Solver and its new features to advance your coding to the next level. Especially with the new built-in and custom type-function features.
What are Type Functions?
Type functions are functions that run during analysis time, and they operate on types instead of runtime values. Basically, they are functions that can transform types and even return new types, depending on what you want to accomplish. Type functions allow you to do achieve things that were not possible to do in the old-solver, such as inheritance, overloading, and more.
Built-in type functions
There are certain built-in type functions that you can already use to manipulate types, such as, keyof, rawkeyof
.
keyof<T>
This type function returns an union
type out of the keys of the given table type.
For example:
local animals = {
cat = { speak = function() print "meow" end },
dog = { speak = function() print "woof woof" end },
monkey = { speak = function() print "oo oo" end },
fox = { speak = function() print "gekk gekk" end }
}
type AnimalType = keyof<typeof(animals)> -- "cat" | "dog" | "monkey" | "fox"
rawkeyof<T>
Works the same as keyof
, except it ignores metatables.
For example:
local tbl = setmetatable({test = ""}, {__index = {test_2 = ""}})
type a = keyof<typeof(tbl)> -- "test" | "test_2"
type b = rawkeyof<typeof(tbl)> -- "test"
index<T, U>
This function returns an union
type out of the values of the given table type. Unlike keyof
and rawkeyof
, this function requires 2 arguments: A table and a key type.
For example:
type Person = {
age: number,
name: string,
alive: boolean
}
type idxType = index<Person, keyof<Person>> -- number | string | boolean
type idxType2 = index<Person, "age" | "name"> -- number | string
Here, we gave the first argument as the Person
table type to the index
function. The first argument is the table that we are going to look up the types of values from. The second argument is the key type(s) that the lookup will be made with. This returns the type(s) of values found as an union
.
rawget<T, U>
Works the same as index
, except it ignores metatables.
setmetatable<T, U>
This type function works similarly to the main-runtime version of itself, except in the type-runtime, it creates a metatable
type.
local clock = {}
type Identity = typeof(setmetatable({} :: { time: number }, { __index = clock }))
is now reduced to:
local clock = {}
type Identity = setmetatable<{ time: number }, { __index: typeof(clock) }> -- evaluates to { time: number, @metatable: { __index: typeof(clock) } }
getmetatable<T>
Like setmetatable
, it works similarly to the main-runtime version of itself, except in the type-runtime, it returns the metatable
type of the given type.
local animal = {}
type Identity = setmetatable<{ sound: string }, { __index: typeof(animal) }>
type ReversedIdentity = getmetatable<Identity> -- evaluates to { __index: typeof(animal) }
Custom Type Functions
So far, the built-in type functions already allow us to expand the capabilities of types, but we can take it to a whole new step.
We can create our own type functions that suit our needs, therefore we can use luau code itself to evaluate the logic behind how types are determined.
Here’s a basic type function example:
type function test()
return types.string
end
type testType = test<> -- string
Here, we created a basic type function called test
that returns a string
type.
Just like the built-in type functions, we call custom type functions with <
and >
brackets.
Note: All type functions must return a type when called from outside of type functions. Otherwise, a type-error will be emitted.
(In addition to the types library, which is explained below, type functions also have access to these libraries and functions.)
The types
library
In the previous example, to return a type, we used types
, which is a global library which contains properties that reference built-in types, and functions that allows you to create your own types.
This library can only be used within type-functions, though it is very powerful.
The types library properties:
types.any
types.unknown
types.never
types.boolean
types.buffer
types.number
types.string
types.thread
(More info about the properties can be found here: Type checking - Luau)
Here’s an example type-function that replicates the built-in function keyof
:
type function simple_keyof(ty: type)
-- Ignoring unions or intersections of tables for simplicity.
if not ty:is("table") then -- :is() is a method of the type object (given as the argument) similar to :isA of an Instance.
error("Can only call keyof on tables.")
end
local union = nil
for property in ty:properties() do -- :properties() is a method of a table type instance that allows you to retrieve properties of a table type.
union = if union then types.unionof(union, property) else property -- .unionof is a function of the types library that creates an union type.
end
return if union then union else types.singleton(nil) -- .singleton is a function that creates a string literal (a.k.a singleton), such as "test".
end
type person = {
name: string,
age: number,
}
--- keys = "age" | "name"
type keys = simple_keyof<person>
Here, we replicated the functionality of the keyof
built-in type function with our own custom type function. Our function takes in a table
type, which gets represented as a type
instance in the type-runtime.
The type
instance
Types are represented as type instances in the type-runtime. This way, you can use them with luau code that allows you to change them or even create your own.
type
instances can have extra properties and methods described depending on its tag.
type.tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "class" | "thread" | "buffer"
To see all methods that a type instance can have, you can visit this page: Type checking - Luau. In the next examples, I will mention some of the most popular ones.
Singleton type
instance
For those who don’t know, singletons are types that represents one single value at runtime.
Strings and booleans are representable as singleton types, however, we generally use strings to represent singletons.
To create a singleton, you use the types.singleton()
function.
types.singleton(arg: string | boolean | nil): type
Table type
instance
Table type
instances represent table types in the type runtime, and have certain special methods allowing you to transform them to whatever you wish.
To create a table type
instance, you use the types.newtable()
function.
Read and Write types
Table properties have separate types for reading from and writing to them. We call these types read
and write
types. The read
type allows you to read from the property, whereas the write
type allows you to write to the property. If a property has both of these types, it can both be read from and written into.
Read-only properties
Suppose you have a property called name
, and you wish to indicate that this property can only be read from, and not be written on to. Using the new type-solver, you can specifically define this property to be read-only, using a special keyword called read
. This operation removes the write
type from the property, which makes it read-only.
type Person = {
read name: string,
age: number
}
local a: Person = {name = "", age = 18}
a.name = "" -- Type Error: Property name of the table 'Person' is read-only.
Write-only properties
The idea is the same as read-only properties, except this time we want to make a property write-only, this means it cannot be read from. Using the keyword write
, you can make a property write-only. This operation removes the read
type from the property, which makes it write-only.
type Person = {
write name: string,
age: number
}
local a: Person = {name = "", age = 18}
print(a.name) -- Type Error: Property name of table '{...}' is write-only.
setreadproperty(key: type, value: type?)
Sets the type for reading from the property named by key
, leaving the type for writing this property as-is. The key
parameter must be a singleton string type
.
-
value
will be set as theread type
, thewrite type
will be unchanged. - If
key
is not already present, only aread type
will be set, making the property read-only. - If
value
isnil
, the property is removed.
setwriteproperty(key: type, value: type?)
Sets the type for writing to the property named by key
, leaving the type for reading this property as-is. The key
parameter must be a singleton string type
.
-
value
will be set as thewrite type
, theread type
will be unchanged. - If
key
is not already present, only awrite type
will be set, making the property write-only. - If
value
isnil
, the property is removed.
setproperty(key: type, value: type?)
Sets key-value pair in the table’s properties, with the same type for reading from and writing to the table. The key
parameter must be a singleton string type
.
type function test()
local newTable = types.newtable()
newTable:setproperty(types.singleton("hi"), types.any)
return newTable
end
type testType = test<> -- {hi: any}
In this example above, we created a new table type
instance, using the types.newtable()
function. This function creates a new type
instance with the “table” tag
, that comes with special methods that you can use. For example, :setproperty()
being one of them, allows you to set the types of the properties within the table type. Using this, we set the key to the singleton string type "hi"
using the types.singleton()
function, and the value type of that key to any
using the any
property of the types
library.
readproperty(key: type): type?
Returns the type used for reading values from this property, or nil
if the property doesn’t exist.
type function customindex(tbl: type, key: type)
if not tbl:is("table") then error("The first given type isn't a table!") end
if not key:is("singleton") then error("The second given type isn't a singleton!") end
local property = tbl:readproperty(key)
if not property then error("Given property does not exist on the given table type.") end
return property
end
type Person = {
name: string,
age: number
}
type indexResult = customindex<Person, "name"> -- string
In the example above, we created a basic custom index
function (that does not create an union
type) that returns the type
of the found property, if it exists within the table type
.
We first make sure to validate the types of the given types, tbl
must be a table type
, and key
must be a singleton type
. And then we use the :readproperty()
method of the table type
with the given singleton type
to get the property, and return it.
This function only works on properties that have a read
type. So, only properties that aren’t write-only will be returned.
writeproperty(key: type): type?
Returns the type used for writing values to this property, or nil
if the property doesn’t exist.
This function works the same as to readproperty()
, except it only returns properties that aren’t read-only, because it only works on properties that have a write
type.
setindexer(index: type, result: type)
Sets the table’s indexer, using the same type for reads and writes.
Indexers are the default types that return the result type of an index operation on a table.
type function createTable()
local newTable = types.newtable()
newTable:setindexer(types.number, types.string)
return newTable
end
type tableType = createTable<> -- {[number]: string}
In the example above, we created a new table type and set its indexer to {[number]: string}
, which means when we index this table with a number, it will return a value of a string type.
(More methods of the table type instance can be found here: Type checking - Luau)
Function type
instance
The function type
instance represents function types in the type-runtime.
setparameters(head: {type}?, tail: type?)
Sets the function’s parameters, with the ordered parameters in head
and the variadic tail in tail
.
type function createFunction()
local newFunction = types.newfunction()
newFunction:setparameters({
types.string,
types.number
})
return newFunction
end
type functionType = createFunction<> -- (string, number) -> ()
In the example above, we created a new function type instance using the types.newfunction()
function, and set its parameters by using :setparameters()
. This function takes a head
table, that has type
s that determine the types of the parameters of the function. Unfortunately, you cannot set the names of the parameters currently, and can only set the types in order.
You can also use the tail
parameter to create a variadic parameter for the function type:
type function createFunction()
local newFunction = types.newfunction()
newFunction:setparameters({
types.string,
types.number
}, types.boolean)
return newFunction
end
type functionType = createFunction<> -- (string, number, ...boolean) -> ()
setreturns(head: {type}?, tail: type?)
Sets the function’s return types, with the ordered parameters in head
and the variadic tail in tail
.
type function createFunction()
local newFunction = types.newfunction()
newFunction:setparameters({
types.string,
types.number
}, types.boolean)
newFunction:setreturns({
types.boolean,
types.any
})
return newFunction
end
type functionType = createFunction<> -- (string, number, ...boolean) -> (boolean, any)
In the example above, we’ve added the return types of the function, using the :setreturns()
function. Like :setparameters()
, it takes a head
table, and can also take a type in its tail
parameter, to create a variadic return type.
setgenerics(generics: {type}?)
Sets the function’s generic type
s.
type function pass()
local T = types.generic("T") -- T
local Us, Vs = types.generic("U", true), types.generic("V", true) -- U..., V...
local f = types.newfunction()
f:setparameters({T}, Us); -- (T, U...)
f:setreturns({T}, Vs); -- (T, V...)
f:setgenerics({T, Us, Vs}); -- <T, U..., V...>
return f
end
type X = pass<> -- <T, U..., V...>(T, U...) -> (T, V...)
In the example above, we created a new function type that contains generic types, and type packs.
To create a generic function type
, we first create a generic type
, by using the types.generic(name: string?, ispack: boolean?)
function. This function can either create a generic type, or a generic type pack.
Using this function, we first created a generic called T
, then we created 2 generic type packs called: U
and V
. Then we created a new function, and set its parameters to the T
generic and the U
generic type pack, and then set its returns to the T
generic and the V
generic type pack.
Finally, using the :setgenerics()
method, we set the function’s generics to the T
generic, and the U
and V
generic type packs, giving us the final function type that is set to the X
type alias.
(More methods of the function type instance can be found here: Type checking - Luau)
Class type
instance
This is a special type that cannot be created manually, as it represents classes that are defined outside of the Luau runtime. Instance
for example, is a Roblox Engine Class that does not exist within the Luau runtime itself. These classes can only be defined in Luau definition files that only runtime embedders can use. But we can still operate on them.
properties(): { [type]: { read: type?, write: type? } }
Returns the properties of the class with their respective read
and write
types.
metatable(): type?
Returns the class’ metatable, or nil
if it doesn’t exist.
classtype:indexer(): { index: type, readresult: type, writeresult: type }?
Returns the class’ indexer, or nil
if it doesn’t exist.
(More methods of the class type instance can be found here: Type checking - Luau)
For more information about the types of the type
instance, you can visit here: Type checking - Luau
General Examples and Places where you can use Custom Type Functions
Coming Soon
What is next?
Coming Soon