[TNLØ Tutorial] Luau Type Functions: Beyond Basic Typechecking

[TNLØ Tutorial] Luau Type Functions: Beyond Basic Typechecking

:warning: This tutorial assumes you already know “static” typechecking.

Quick terminology recap:

type Union = "Hello"|number|string|"More..."

type SingletonString = "Hello"
type SingletonBoolean = true
--type SingletonNil = nil--nil is always a singleton
type SingletonNumber = 5--(soon? not added yet)
type SingletonString = "Hi"

type PrimitiveBoolean = boolean
type PrimitiveNumber = number
type PrimitiveString = string
type PrimitiveClass = Instance--Always primitive

type intersection = {Parent:Instance} & {Name:string}
type Inheritance_Or_Class = DataModel-- More in detail when we go to type functions

Also, the documentation and API refer to varargs/tuples/… as a tail.

The arguments before the tail are referred to as the function head.

TL;DR:

(Arg1,Arg2,Arg3) - head
(...) - tail

The documentation also uses the term “Table Indexer”. It refers to a table with a non-singleton defined key, such as:

{
	[number]: string
}
{
	[string]: number
}
{
	[number]: boolean
}

Note: This tutorial covers only what the Order deems useful. Metatable-related bloat will be deliberately excluded, as it stands against optimization—and therefore against the interests of both developers and users.

Death before bloat.


What is a type function, and why would I ever use it?

Type functions are purely used to help you with autocomplete and analysis without having to pay any runtime cost. Instead, the editor runs them through a type system to scan for errors and provide autocomplete results. Currently (November 9th, 2026), many useful features like singleton generics and singleton numbers are missing, severely limiting the number of use cases. However, there are already plenty of use cases.

Before we write any of that, let me quickly walk through some existing type functions from the old and new type solvers that could be useful.


typeof()

Although there is an identically named function at runtime, it behaves differently when used in a type annotation. Let’s take this example:

local Hello: typeof(game)-->DataModel

As we know, the actual type of the variable game is a DataModel (print(game.ClassName) ), so typeof has retrieved this type and assigned it to our variable Hello.

Pretty simple! It works with just about anything and, most importantly, works with actual functions. We’ll get to that later. You can pass real instances or data types to it, and it will extract their data type.

--Description
local CoolFunction = function(Data:Instance):string
	return "Lol"
end

local a:typeof(CoolFunction)-->a(Data: Instance): string

a now has the same type and autocomplete as CoolFunction. It also captures function descriptions, by the way (and attributes like @deprecated).

local a:typeof("Lol")-->string

Really useful if you don’t want to write the type manually.


Modern type functions

Modern type functions are called using <> brackets instead of () brackets.

index<Type, Key>

Ever wanted to get the type of, let’s say, a method or a value of a type from a key? Not a problem!

local a: index<Player,"Kick">-->a(self: Player, message: string?): ()

Now we have the Kick method’s type in a variable.

local a: index<DataModel,"Name">-->string

This specific case can be simplified using typeof:

local a: index<typeof(game),"Name">-->string

keyof<Type>

Returns all keys of a type as a union.

Also, it’s important to note, as we saw earlier, that a variable IS NOT A TYPE (unless we talk about type function envirement then its a little different). Use typeof() to extract a TYPE from a variable.

local a:keyof<DataModel>-->"AddTag" | "AllowedGearTypeChanged" | "AncestryChanged" | "Archivable" | "AttributeChanged" | "BindToClose" | "Capabilitie…

There are more, but they’re metatable-related, and as we know, metatables are morally evil or something, so we skip them.


Your Own Type Functions

So, let’s define our type function test, which will accept one argument.

type function test(Arg1:type)

end

Also, note that you can have more than one argument, obviously, but not a tuple (as of now).

Now, let’s discuss what the “type” type actually contains:

Property Meaning
tag Primitive representation of a type, e.g. “Hello” would be “string” (Instances and Roblox data types are always extern)

Methods & types library

:warning: 9th of November, 2026: Bug — no autocomplete for the types library.


types library

This library is used to construct type.

types.any, types.unknown, types.never, types.boolean, types.buffer, types.number, types.string, types.thread, types.singleton(nil)
All return the primitive type.

.tag = "any", "unknown", "never", "boolean", "buffer", "number", "string", "thread", "nil"

Some types, like “Instance”, are not part of this library, so you need some hacky ways to extract them, like this:

type _Instance = Instance
type function test(Arg1:type)
	return _Instance
end

Creating singleton (“”) types.singleton

You can create singletons from strings, booleans, or nil (possibly numbers in the future).

.tag = "singleton"

types.singleton("Hey") --> "Hey"
types.singleton(true) --> true
types.singleton(false) --> false

Creating negation (~) types.negationof

You can create a type that accepts everything except the specified type (~type).

.tag = "negation"

types.negationof(types.singleton("Hey")) --> ~"Hey"
types.negationof(_Instance) --> ~Instance
types.negationof(types.string) --> ~string

Creating optional (?) types.optional

Makes the type accept either itself or nil.

.tag = "union"

types.negationof(types.singleton("Hey")) --> "Hey"?
types.negationof(_Instance) --> Instance?
types.negationof(types.string) --> string?

Creating Union (|) types.unionof

Lets you create or expand a union type.

.tag = "union"

types.unionof(types.number,types.string,types.buffer) --> string | number | buffer
types.unionof(types.singleton("Hey"),types.singleton("Hello")) --> "Hey" | "Hello"
types.unionof(types.singleton("Hey"),types.singleton("Hello"),types.boolean) --> "Hey" | "Hello" | boolean

Creating Intersection (&) types.intersectionof

Creates an intersection of table or function types.

.tag = "intersection"

type lol = {Size:number}
type Funny = {Parent:Instance}
type Yeah = {Name:string}

type function test(Arg1:type)
	return types.intersectionof(lol,Funny,Yeah)
end
type func1 = ()->string
type func2 = (Name:string)->number
type func3 = (Name:string,Value:any)->()

type function test(Arg1:type)
	return types.intersectionof(func1,func2,func3)
end

Creating Empty Table ({}) types.newtable

.tag = "table"

Creates an empty mutable table type.

types.newtable()
types.newtable(nil,{index=types.number,readresult=types.string,writeresult=types.string})
-- {[number]: string} or just {string}

Creating Empty Function (function() end) types.newfunction

Creates an empty mutable function type.

.tag = "function"

types.newfunction()
types.newfunction({head={types.string,types.number}},{head={types.string}})
-- (string,number) -> string

Creating Copy (copy) types.copy

Creates a copy of a type.

types.copy(types.string)

Creating Generic Function Argument (<T>) types.generic

Creates a generic argument using the name provided as the first argument. The second parameter (a boolean) makes it a tuple/vararg/tail/pack.

.tag = "generic"

types.generic("T")
types.generic("U",true)

Universal type methods

:is(string)

Returns true if .tag is equal to the argument; otherwise, returns false.

types.newtable():is("table")
--true
types.string:is("table")
--false

:issubtypeof(type)

Same as is:(), but accepts an actual type and works with the hierarchy of parent types (see Extern).

Arg1:issubtypeof(_Instance)
--true
types.singleton("Hi"):issubtypeof(types.string)
--true
types.singleton("Hi"):issubtypeof(types.number)
--false

Singleton related

:value()

Returns the actual value of a singleton type.

types.singleton("Hi"):value()
--"Hi"

Generics related

:name()

Returns the name of a generic.

local generic = types.generic("T")
generic:name()
--"T"

:ispack()

Returns true/false depending on whether the generic is a tuple/tail/vararg.

local T = types.generic("T",true)
T:ispack()
--true
local V = types.generic("V")
V:ispack()
--false

Roblox instance related (extern tag related) methods

:properties()

Useful for iteration.

:warning: NOTE THAT KEYS ARE SINGLETON TYPES*

type function test(Arg1:type)
	for KeyType in Arg1:properties()--[[{"FormFactor":{}, "formFactor":{}}]] do
		
	end
end

local t:test<FormFactorPart>

:readparent()

As we know, Roblox instances have a type hierarchy:

Object → Instance → PVInstance → BasePart → FormFactorPart → Part

This method returns the parent type. For example:

Part → FormFactorPart
BasePart → PVInstance

If there is no parent, as with Object, it returns nil.

Other methods behave similarly to the table-related ones.

https://luau.org/types-library/#extern-type-instance


Union type related

:components()

Creates an array from a union type.

type function test(Arg1:type)
	for _,Type in Arg1:components()--[[{"Hello","Yes","Union types are cool"}]] do
		
	end
end

local a: test<"Hello"|"Yes"|"Union types are cool">

Table type related

:properties()

Returns all singleton keys of a table.

:warning: NOTE THAT KEYS ARE SINGLETON TYPES*

type function test(Arg1:type)
	for KeyType in Arg1:properties()--[[{Lol:{},Funny:{},A:{}}]] do
		
	end
end
local a: test<{Lol:number,Funny:boolean,A:string}>

:setproperty(Key, Value)

Allows you to set a key’s type equal to a value. THE KEY MUST BE A SINGLETON.

local t = types.newtable()
t:setproperty(types.singleton("Hi"),types.number)
-- {Hi: number}

:readproperty(Key)

Allows you to retrieve the index value type using a key that must be a singleton.

t:readproperty(types.singleton("Hi"))
-- number

:setindexer(Key,Value)

Sets the indexer of a table.

t:setindexer(types.string,types.number)
--{[string]:number}

:indexer()

Gets the indexer of a table, or nil if one does not exist.

t:indexer()
--{ index: types.string, readresult: types.number, writeresult: types.number}

Function type related

:setparameters(head?, tail?)

Sets the types of arguments in the head, with an optional tail (tuple/varargs/3 dots).

local func = types.newfunction()
func:setparameters({types.string,types.number},types.number)
--(_: string, _: number, ...: number): ()

:parameters()

Returns the function’s arguments.

func:parameters()
--{ head: {types.string,types.number}, tail: types.number }

:setreturns(head?, tail?)

Works like :setparameters(), but for return values.

local func = types.newfunction()
func:setreturns({types.string,types.number},types.number)
--(): (string, number, ...number)

:returns()

Works like :parameters(), but for return values.

func:parameters()
--{ head: {types.string,types.number}, tail: types.number }

:setgenerics({T})

You can use generics as types, but you must use this function to register them in the correct order, e.g. <T, P, A>.

local T,P,A = types.generic("T"),types.generic("P"),types.generic("A",true)
local func = types.newfunction()
func:setparameters({T,P},A)
func:setreturns(nil,A)
func:setgenerics({T,P,A})
--(_: T, _: P, ...: A...): A...

:generics()

Returns the function’s generics.

func:generics()
--{T,P,A}

Negation type related

:inner()

Removes negation.

local t = types.negationof(types.singleton("A"))
-- ~"A"
t:inner()
--"A"
local t = types.negationof(types.unionof(types.singleton("A"),types.singleton("B")))
-- ~("A" | "B")
t:inner()
--"A" | "B"

Intersection type related

:components()

Behaves the same way as it does for union types.

type intersection = (()->string) & ((Name:string)->number) & ((Name:string,Value:any)->())

type function test(Arg1:type)
	Arg1:components()
end
--{()->string), (Name:string)->number), (Name:string,Value:any)->()}

Type Function Environment

List of functions available inside type functions:

https://luau.org/types/type-functions/#type-function-environment

How do you use print inside a type function?

Very easily! Every time you use print and hover over the type function call, the editor displays its output:


Creating example type function with gained knowledge

Lets create a type function + type for it to retrive properties of an instance to get auto completion

type _Instance = Instance
type function Properties(ins:type)
	if not ins:issubtypeof(_Instance)then
		error("Has to be an Instance")
	end
	
	local union = types.string;
	
	local CurrentParent = ins;
	while CurrentParent do
		for property in CurrentParent:properties() do
			union = types.unionof(union, property)
		end;
		CurrentParent = CurrentParent:readparent()
	end 
	return union;
end

local GetProperty = select(2,xpcall(function():()
	return (game::any)[nil] 
end,function():()
	return debug.info(2,"f")
end))::typeof(--[[Get property of an instance]]
	function<T>(instance: T, property: Properties<T>):any return end)


local Name = GetProperty(game,"Name")

Sadly we cannot get also type of a property because we dont have singleton generics yet but we can do something about it if we change:

::typeof(--[[Get property of an instance]]
	function<T>(instance: T, property: Properties<T>):any return end)

to:

To what you may ask? No clue myself i lost electricity mid write (due to a certain country eastward bombing me or something) so i post it from my phone rn thanks to a devforum drafts feature to recover the post. So yeah im pretty sure it was kind of lame solution anyway.
If you spot issues in the tutorial let me know i really tried to finish it before lights out so you know i was in a hurry.


TNLØ — Ministry of Education & Optimization

Educating developers. Eliminating bloat. Advancing optimization.

9th of November, 2026

Written by hand with AI helping to fix grammar (like my other tutorials) by @Yarik_superpro
The Grand Optimizer

Nothing lost. Nothing gained. Just the truth remained.

3 Likes

Damn, Yarik found using generics in types, congratulations on leveling up your type checkering skills