npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@rbxts/t

v3.1.1

Published

A Runtime Type Checker for Roblox

Downloads

3,702

Readme

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[, childTable])
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))