Novice, Practiced, Mastered

    @rbxts/t
    TypeScript icon, indicating that this package has built-in type declarations

    3.0.0 • Public • Published

    t

    A Runtime Type Checker for Roblox
     

    t is a module which allows you to create type definitions to check values against.

    Download

    You can download the latest copy of t here.

    Why?

    When building large systems, it can often be difficult to find type mismatch bugs.
    Typechecking helps you ensure that your functions are recieving the appropriate types for their arguments.

    In Roblox specifically, it is important to type check your Remote objects to ensure that exploiters aren't sending you bad data which can cause your server to error (and potentially crash!).

    Crash Course

    local t = require(path.to.t)
    
    local fooCheck = t.tuple(t.string, t.number, t.optional(t.string))
    local function foo(a, b, c)
    	assert(fooCheck(a, b, c))
    	-- you can now assume:
    	--	a is a string
    	--	b is a number
    	--	c is either a string or nil
    end
    
    foo() --> Error: Bad tuple index #1: string expected, got nil
    foo("1", 2)
    foo("1", 2, "3")
    foo("1", 2, 3) --> Error: Bad tuple index #3: (optional) string expected, got number

    Check out src/t.spec.lua for a variety of good examples!

    Primitives

    Type Member
    boolean => t.boolean
    thread => t.thread
    function => t.callback
    nil => t.none
    number => t.number
    string => t.string
    table => t.table
    userdata => t.userdata

    Any primitive can be checked with a built-in primitive function.
    Primitives are found under the same name as their type name except for two:

    • nil -> t.none
    • function -> t.callback

    These two are renamed due to Lua restrictions on reserved words.

    All Roblox primitives are also available and can be found under their respective type names.
    We won't list them here to due how many there are, but as an example you can access a few like this:

    t.Instance
    t.CFrame
    t.Color3
    t.Vector3
    -- etc...

    You can check values against these primitives like this:

    local x = 1
    print(t.number(x)) --> true
    print(t.string(x)) --> false, "string expected, got number"

    Type Composition

    Often, you can combine types to create a composition of types.
    For example:

    local mightBeAString = t.optional(t.string)
    print(mightBeAString("Hello")) --> true
    print(mightBeAString()) --> true
    print(mightBeAString(1)) --> false, "(optional) string expected, got number"

    These get denoted as function calls below with specified arguments. check can be any other type checker.

    Meta Type Functions

    The real power of t is in the meta type functions.

    t.any
    Passes if value is non-nil.

    t.literal(...)
    Passes if value matches any given value exactly.

    t.keyOf(keyTable)
    Returns a t.union of each key in the table as a t.literal

    t.valueOf(valueTable)
    Returns a t.union of each value in the table as a t.literal

    t.optional(check)
    Passes if value is either nil or passes check

    t.tuple(...)
    You can define a tuple type with t.tuple(...).
    The arguments should be a list of type checkers.

    t.union(...) - ( alias: t.some(...) )
    You can define a union type with t.union(...).
    The arguments should be a list of type checkers.
    At least one check must pass
    i.e. t.union(a, b, c) -> a OR b OR c

    t.intersection(...) - ( alias: t.every(...) )
    You can define an intersection type with t.intersection(...).
    The arguments should be a list of type checkers.
    All checks must pass
    i.e. t.intersection(a, b, c) -> a AND b AND c

    t.keys(check)
    Matches a table's keys against check

    t.values(check)
    Matches a table's values against check

    t.map(keyCheck, valueCheck)
    Checks all of a table's keys against keyCheck and all of a table's values against valueCheck

    There's also type checks for arrays and interfaces but we'll cover those in their own sections!

    Special Number Functions

    t includes a few special functions for checking numbers, these can be useful to ensure the given value is within a certain range.

    General:
    t.nan
    determines if value is NaN
    All of the following checks will not pass for NaN values.
    If you need to allow for NaN, use t.union(t.number, t.nan)

    t.integer
    checks t.number and determines if value is an integer

    t.numberPositive
    checks t.number and determines if the value > 0

    t.numberNegative
    checks t.number and determines if the value < 0

    Inclusive Comparisons:
    t.numberMin(min)
    checks t.number and determines if value >= min

    t.numberMax(max)
    checks t.number and determines if value <= max

    t.numberConstrained(min, max)
    checks t.number and determines if min <= value <= max

    Exclusive Comparisons:
    t.numberMinExclusive(min)
    checks t.number and determines if value > min

    t.numberMaxExclusive(max)
    checks t.number and determines if value < max

    t.numberConstrainedExclusive(min, max)
    checks t.number and determines if min < value < max

    Special String Functions

    t includes a few special functions for checking strings

    t.match(pattern)
    checks t.string and determines if value matches the pattern via string.match(value, pattern)

    Arrays

    In Lua, arrays are a special type of table where all the keys are sequential integers.
    t has special functions for checking against arrays.

    t.array(check)
    determines that the value is a table and all of it's keys are sequential integers and ensures all of the values in the table match check

    Interfaces

    Interfaces can be defined through t.interface(definition) where definition is a table of type checkers.
    For example:

    local IPlayer = t.interface({
    	Name = t.string,
    	Score = t.number,
    })
    
    local myPlayer = { Name = "TestPlayer", Score = 100 }
    print(IPlayer(myPlayer)) --> true
    print(IPlayer({})) --> false, "[interface] bad value for Name: string expected, got nil"

    You can use t.optional(check) to make an interface field optional or t.union(...) if a field can be multiple types.

    You can even put interfaces inside interfaces!

    local IPlayer = t.interface({
    	Name = t.string,
    	Score = t.number,
    	Inventory = t.interface({
    		Size = t.number
    	})
    })
    
    local myPlayer = {
    	Name = "TestPlayer",
    	Score = 100,
    	Inventory = {
    		Size = 20
    	}
    }
    print(IPlayer(myPlayer)) --> true

    If you want to make sure an value exactly matches a given interface (no extra fields),
    you can use t.strictInterface(definition) where definition is a table of type checkers.
    For example:

    local IPlayer = t.strictInterface({
    	Name = t.string,
    	Score = t.number,
    })
    
    local myPlayer1 = { Name = "TestPlayer", Score = 100 }
    local myPlayer2 = { Name = "TestPlayer", Score = 100, A = 1 }
    print(IPlayer(myPlayer1)) --> true
    print(IPlayer(myPlayer2)) --> false, "[interface] unexpected field 'A'"

    Roblox Instances

    t includes two functions to check the types of Roblox Instances.

    t.instanceOf(className[, childTable])
    ensures the value is an Instance and it's ClassName exactly matches className
    If you provide a childTable, it will be automatically passed to t.children()

    t.instanceIsA(className)
    ensures the value is an Instance and it's ClassName matches className by a IsA comparison. (see here)

    t.children(checkTable)
    Takes a table where keys are child names and values are functions to check the children against.
    Pass an instance tree into the function.

    Warning! If you pass in a tree with more than one child of the same name, this function will always return false

    Roblox Enums

    t allows type checking for Roblox Enums!

    t.Enum
    Ensures the value is an Enum, i.e. Enum.Material.

    t.EnumItem
    Ensures the value is an EnumItem, i.e. Enum.Material.Plastic.

    but the real power here is:

    t.enum(enum)
    This will pass if value is an EnumItem which belongs to enum.

    Function Wrapping

    Here's a common pattern people use when working with t:

    local fooCheck = t.tuple(t.string, t.number, t.optional(t.string))
    local function foo(a, b, c)
    	assert(fooCheck(a, b, c))
    	-- function now assumes a, b, c are valid
    end

    t.wrap(callback, argCheck)
    t.wrap(callback, argCheck) allows you to shorten this to the following:

    local fooCheck = t.tuple(t.string, t.number, t.optional(t.string))
    local foo = t.wrap(function(a, b, c)
    	-- function now assumes a, b, c are valid
    end, fooCheck)

    OR

    local foo = t.wrap(function(a, b, c)
    	-- function now assumes a, b, c are valid
    end, t.tuple(t.string, t.number, t.optional(t.string)))

    Alternatively, there's also: t.strict(check)
    wrap your whole type in t.strict(check) and it will run an assert on calls.
    The example from above could alternatively look like:

    local fooCheck = t.strict(t.tuple(t.string, t.number, t.optional(t.string)))
    local function foo(a, b, c)
    	fooCheck(a, b, c)
    	-- function now assumes a, b, c are valid
    end

    Tips and Tricks

    You can create your own type checkers with a simple function that returns a boolean.
    These custom type checkers fit perfectly with the rest of t's functions.

    If you roll your own custom OOP framework, you can easily integrate t with a custom type checker.
    For example:

    local MyClass = {}
    MyClass.__index = MyClass
    
    function MyClass.new()
    	local self = setmetatable({}, MyClass)
    	-- setup instance
    	return self
    end
    
    local function instanceOfClass(class)
    	return function(value)
    		local tableSuccess, tableErrMsg = t.table(value)
    		if not tableSuccess then
    			return false, tableErrMsg or "" -- pass error message for value not being a table
    		end
    
    		local mt = getmetatable(value)
    		if not mt or mt.__index ~= class then
    			return false, "bad member of class" -- custom error message
    		end
    
    		return true -- all checks passed
    	end
    end
    
    local instanceOfMyClass = instanceOfClass(MyClass)
    
    local myObject = MyClass.new()
    print(instanceOfMyClass(myObject)) --> true

    Known Issues

    You can put a t.tuple(...) inside an array or interface, but that doesn't really make any sense..
    In the future, this may error.

    Notes

    This library was heavily inspired by io-ts, a fantastic runtime type validation library for TypeScript.

    Why did you name it t?

    The whole idea is that most people import modules via:
    local X = require(path.to.X)
    So whatever I name the library will be what people name the variable.
    If I made the name of the library longer, the type definitions become more noisy / less readable.
    Things like this are pretty common:
    local fooCheck = t.tuple(t.string, t.number, t.optional(t.string))

    Keywords

    none

    Install

    npm i @rbxts/t

    DownloadsWeekly Downloads

    187

    Version

    3.0.0

    License

    ISC

    Unpacked Size

    47.4 kB

    Total Files

    5

    Last publish

    Collaborators

    • osyris