Thoughts on type checking system?

Typer

I’m currently working on a strict type checking system called “typer” that will be open-sourced. Below is some example code using this system! At the bottom of this post I also attached the module source code.

Purpose

I want this module to make type checking easy and precise. This shouldn’t cause a huge interruption to the workflow and should allow for better debugging and stricter values.

Feedback I’m looking for

I’m looking for any feedback that can address any of the following problems, if applicable:

  • Design flaws
  • Workflow flaws
  • Naming sheme
  • Usability

Examples

Type casting

Code:

local typer = require(script.Parent.typer)
local cast = typer.cast

local valueA = cast("hello!") "string"
local valueB = cast(10) "number"

print("valueA", valueA)
print("valueB", valueB)

local valueC = cast("foo") "table"

print("valueC", valueC)

Output:

image

Casted value EQ

Code:

local typer = require(script.Parent.typer)
local cast = typer.cast
local casteq = typer.casteq

local valueA = cast("hello!") "string"
local valueB = "foo"
local valueC = cast(10) "number"
local valueD = "foo"

if casteq(valueA, valueB) then
	print("valueA = valueB")
end

if casteq(valueB, valueD) then
	print("valueA = valueD")
end

if casteq(valueB, valueC) then
	print("valueB = valueC")
end

Output:

image

Immutable environments

Code:

local typer = require(script.Parent.typer)
local cast = typer.cast
local env = typer.env

local testEnvironment = env()
testEnvironment.a = 5
testEnvironment.b = 7
testEnvironment.c = 12

testEnvironment("printValue", "a")
testEnvironment("printAll")
testEnvironment("dumpValue", "a")
testEnvironment("printValue", "a")
testEnvironment("dumpAll")
testEnvironment("printAll")

testEnvironment.a = 8
testEnvironment.b = 19
testEnvironment.b = 21

Output:

image

Soft casting

Code:

local typer = require(script.Parent.typer)
local expect = typer.expect

expect(5, 12, 15).toBe("number", "number", "number").andIfSo(function()
	print("all types match in function #1")
end).andIfNot(function(output)
	print("not all types match in function #1, output:", output)
end)

expect(5, nil, {1, 2, 3}, "hiya").toBe("number", "nil", "number", "string").andIfSo(function()
	print("all types match in function #2")
end).andIfNot(function(output)
	print("not all types match in function #2, output:", output)
end)

Output:

image

Tuple assertion

Code:

local typer = require(script.Parent.typer)
local tupleAssertion = typer.tupleAssertion

local assertType1 = tupleAssertion("number", "number")
local assertType2 = tupleAssertion("string", "table", "number")

local function testFunction1(a, b)
	assertType1(a, b)
	return a + b -- Can safely do this if the above doesn't error
end

local function testFunction2(a, b, c)
	assertType2(a, b, c)
	return string.len(a) + #b + c -- Can safely do this if the above doesn't error
end

local value1 = testFunction1(1, 5)
print(value1)

local value2 = testFunction2("hello world!", {"a", 1, true}, 4)
print(value2)

local value3 = testFunction2("oops!", 2, {})
print(value3)

Output:

image

Static functions

Code:

local typer = require(script.Parent.typer)
local static = typer.static

local testFunction = static(function(a, b, c)
	return a + b, if c == true then 1 else "erp"
end).withParameterTypes("number", "number", "boolean").withReturnTypes("number", "number").asserted()

local value1 = testFunction(1, 2, true)
print(value1)

local value2 = testFunction(3, 4, false)
print(value2)

local value3 = testFunction("this would also normally error")
print(value3)

Output:

image

Source

Source code module
local typer = {}

function typer.static(callbackFunction : (any?) -> any?)
	local self = {}
	local parameterAssertion = nil
	local returnAssertion = nil
	
	function self.withParameterTypes(...)
		parameterAssertion = typer.tupleAssertion(...)
		return self
	end
	
	function self.withReturnTypes(...)
		returnAssertion = typer.tupleAssertion(...)
		return self
	end
	
	function self.asserted()
		if parameterAssertion and returnAssertion then
			return function(...)
				local args = {...}
				
				if parameterAssertion then
					parameterAssertion(unpack(args))
				end
				
				local returnedValues = {callbackFunction(...)}

				if returnAssertion then
					local success, output = pcall(function()
						returnAssertion(unpack(returnedValues))
					end)

					if not success then
						error(`An error occurred when expecting return types from static function: {output}`)
					end
				end
				
				return unpack(returnedValues)
			end
		end
	end
	
	return self
end

function typer.cast(value : any) : () -> any
	return function(... : string) : any
		if not table.find({...}, typeof(value)) then
			error(`Expected casted value to be a {table.concat({...}, ", or ")}, instead got a {typeof(value)} type`)
		end
		
		return value
	end
end

function typer.casteq(a : any, b : any)
	if typeof(a) ~= typeof(b) then
		error(`Attempted to compare a {typeof(a)} to a {typeof(b)}`)
	end
	
	return a == b
end

function typer.tupleAssertion(... : string)
	local types = {...}
	
	return function(... : any)
		local args = {...}
		
		for index, type_ in types do
			if typeof(args[index]) ~= type_ then
				error(`Tuple #{index} in function does not match type; expected a {type_}, got a {typeof(args[index])}.`)
			end
		end
	end
end

function typer.expect(... : any)
	local self = {}
	
	local args = {...}
	local success, output
	
	function self.toBe(... : string)
		local types = {...}
		
		success, output = pcall(function()
			typer.tupleAssertion(unpack(types))(unpack(args))
		end)
		
		return self
	end
	
	function self.andIfNot(callback : (any?) -> nil)
		if not success then
			callback(output)
		end
		
		return self
	end
	
	function self.andIfSo(callback : (any?) -> nil)
		if success then
			callback(output)
		end
		
		return self
	end
	
	return self
end

function typer.env() : typeof(setmetatable({},{}))
	local self = {}
	
	return setmetatable({}, {
		__index = function(_, index)
			if not self[index] then
				warn(`Attemped to access an immutable value named "{index}" but it does not exist, returning nil`)
			end
			
			return self[index]
		end,
		
		__newindex = function(_, index, value)
			if self[index] then
				error("Attemped to modify an immutable value")
			end
			
			self[index] = value
			
			if typeof(value) == "Instance" then
				value.AncestryChanged:Connect(function()
					if value:FindFirstAncestor(game) == nil then
						self[index] = nil
					end
				end)
			end
		end,
		
		__call = function(_, command, ...)
			({
				dumpAll = function()
					self = {}
				end,
				
				dumpValue = function(index)
					self[index] = nil
				end,
				
				printAll = function()
					print(self)
				end,
				
				printValue = function(index)
					print(self[index])
				end,
			})[command](...)
		end,
	})
end

return typer

Example place file

I doubt it will be useful in normal roblox scripting. As most instances in the workspace are separate classes, it would be hell to use a strict type system.

1 Like