"t" - A Runtime Type Checker for Roblox

I made this for checking types during runtime. It’s designed specifically for Roblox, but also works in Lua5.1.

Give it a try, let me know if you have any questions or if the documentation could be more clear anywhere.

61 Likes

Added error messages as a second return value from type checkers.

If you wrap your type check in an assert you’ll get the error message on failure:

assert(t.number("1")) --> number expected, got string

(these can get pretty complex based on your type definitions)

Different example:

local success, errMsg = t.number("1")
if success then
	-- do stuff
else
	warn(errMsg)
end
4 Likes

this is something ive always wanted to have but ive always been too lazy to make

very good 10/10

3 Likes

How would you check that a number isn’t nan, +inf, or -inf?

(No reason, really. Just thought that someone may need to know this if the domain of a function is all real numbers. I’m using this library for my game <3)

1 Like

There isn’t a check built in for this, but you could totally make your own. (See tips and tricks in the readme)

I’ll consider adding something like this in the future.

1 Like

This… is just wonderful.

1 Like

Thanks, let me know if anything was confusing with the documentation.

Did you find any cool (or not obvious) uses for it?

Not any particularly non-obvious uses, the documentation is good.

Interestingly enough I made a runtime type checker a while ago.

The main difference is that I wanted to bind signatures to functions themselves, here’s an example.

local myFunction = validate("number", "string", function (a, b)
  print("Valid")
end)
myFunction(1, "Hi")
-- Valid
myFunction("Test")
-- Error: bad argument #1 to 'myFunction' (number expected, got string)

I realized that overrides were also quite important so added in support for them too.

myFunction:AddSignature("string")
myFunction("Test")
-- Valid

There is also support for varargs, so you can specify the type as ... and it’ll match any type.

local myOtherFunction = validate("function", "...", function (f, ...)
   f(...)
end)
3 Likes

If there’s one thing I dislike about this library, it’s the name. Too short and makes it hard to search for (via Google, DevForum search)

1 Like

That’s definitely a downside to the name, but many people dislike having a different variable name from the library name. So a single letter encourages a short variable name. More complex type definitions require you to repeat the variable name often in the same line.

1 Like

Just wanted to dump a bunch of checker I found a use for in my game:

do
	local function isNan(n)
		return n ~= n
	end
	
	function t.nan(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end
		
		if isNan(value) then
			return true
		else
			return false, string.format("NaN expected")
		end
	end
	
	function t.notNan(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end
		
		if not isNan(value) then
			return true
		else
			return false, string.format("non-NaN expected")
		end
	end
end

do
	local function isFinite(n)
		return n > -math.huge and n < math.huge
	end
	
	function t.finite(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end
	
		if isFinite(value) then
			return true
		else
			return false, string.format("finite expected")
		end
	end
	
	function t.infinite(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end
	
		if not isFinite(value) then
			return true
		else
			return false, string.format("infinite expected")
		end
	end
end

function t.real(value)
	local success, errMsg = t.notNan(value)
	if not success then
		return false, errMsg or ""
	end
	
	local success, errMsg = t.finite(value)
	if not success then
		return false, errMsg or ""
	end
	
	return true
end

function t.realVector3(vector)
	local success, errMsg = t.Vector3(vector)
	if not success then
		return false, errMsg or ""
	end
	
	local success, errMsg = t.real(vector.magnitude)
	if not success then
		return false, "vector components are not all real"
	end
	
	return true
end

function t.unitVector3(vector)
	local success, errMsg = t.Vector3(vector)
	if not success then
		return false, errMsg or ""
	end
	if vector.magnitude == 1 then
		return true
	else
		return false, "unit vector expected"
	end
end

t.alpha = t.numberConstrained(0, 1)

function t.integerMin(min)
	assert(t.integer(min))
	return function(value)
		local success, errMsg = t.integer(value)
		if not success then
			return false, errMsg or ""
		end
		if value >= min then
			return true
		else
			return false, string.format("integer >= %d expected", min)
		end
	end
end

function t.integerMax(max)
	assert(t.integer(max))
	return function(value)
		local success, errMsg = t.integer(value)
		if not success then
			return false, errMsg or ""
		end
		if value <= max then
			return true
		else
			return false, string.format("integer <= %d expected", max)
		end
	end
end

function t.integerConstrained(min, max)
	assert(t.integer(min) and t.integer(max))
	local minCheck = t.numberMin(min)
	local maxCheck = t.numberMax(max)
	return function(value)
		local integerSuccess, integerErrMsg = t.integer(value)
		if not integerSuccess then
			return false, integerErrMsg or ""
		end
		
		local minSuccess, minErrMsg = minCheck(value)
		if not minSuccess then
			return false, minErrMsg or ""
		end

		local maxSuccess, maxErrMsg = maxCheck(value)
		if not maxSuccess then
			return false, maxErrMsg or ""
		end

		return true
	end
end

t.nonNegativeInteger = t.integerMin(0)

function t.equals(valueToEquals)
	return function(value)
		if value == valueToEquals then
			return true
		end
		
		return false, "two values are not equal."
	end
end

function t.notEquals(valueToNotEquals)
	return function(value)
		if value ~= valueToNotEquals then
			return true
		end
		
		return false, "two values are not unequal."
	end
end

PS: I’m using this library to make sure that clients are not sending bad data to my remotes. Can you think of any reason why I shouldn’t be doing this with your library?

1 Like

That is a perfectly valid use case! It’s why I originally wrote the library.

And thanks for posting this, these are great examples of custom type checkers.

1 Like

May need to be a little careful with your unitVector3 - the magnitude of a vector produced by .unit or documented as unitary in the API references is not guaranteed to always equal exactly 1 as far as I’m aware. It may instead be off by a very small amount - you could use an epsilon to catch these.

4 Likes

Do you think vector == vector.unit is a good way of checking whether or a vector is a unitvector? Would be odd if the value of vector.unit changed every time I got it.

I’m not sure. I’d suggest instead that you just do something like

local abs = math.abs
local eps = 0.0001
-- stuff 
return abs(vec.magnitude - 1) < eps
1 Like

I suppose. I was hoping to avoid using arbitrary eps but then again it’s not like any of my use cases require such precision.

I was able to reproduce this, which makes sense. Not something I’ve thought about.

local vec = Vector3.new(0.0001, 0.0001, 0.0001).Unit
print(vec.Magnitude) -- 0.99999994039536
1 Like

Wow this is just silly.

local vec = Vector3.new(0.0001, 0.0001, 0.0001).Unit 
print(vec.magnitude) 
vec = vec.Unit
print(vec.magnitude) 

prints

0.99999994039536
1.0000001192093

Doing it again flip-flops between the two values.

1 Like

Two new functions were added:

t.exactly can be used to compare a value to another by ==. Pair this with t.union to create a list of options for a type.

t.strictInterface which allows interface checking that errors with extra fields.

You can find out more about both of these in the project readme.

3 Likes