Corecii / GreenTea

A runtime typechecker with Luau types, pretty errors, and inspection

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

🍵 GreenTea

An experimental runtime typechecker for Roblox Luau, with matching 'tooling-time' Luau types.

Docs

Features

  • Check types at runtime (like the t package)
  • Build Luau types and runtime types simultaneously (so you have less duplicate code!)
  • Pretty errors that tell you exactly where you went wrong, even for unions and intersections
  • Inspect types at runtime, so you can procedurally check types in weird scenarios, such as when checking a set of Instance attributes.
  • Infer types from values
  • t compatibility (via GreenTea.t — all t tests pass!)

Experimental

While this package has had a lot of work put into it, it's largely an experiment. It's a first iteration, so the API can use some improvement — especially the structure of the generated Type objects, which could have a better shape.

Please leave feedback on the issues page. Thank you!

Install

with Wally (for Rojo)

  1. Install Wally
  2. Add GreenTea = "corecii/greentea@0.4.7" to your wally.toml

Standalone (for non-Rojo)

How Does It Work?

GreenTea's runtime code looks like a typical runtime typechecker: you call functions to create typecheckers and compose them together.

It has two tricks:

  1. Its function type definitions lie and say that it's returning the type that you're checking for instead of a GreenTea.Type object. So while you're composing a runtime type, you're also composing a Luau type definition!
  2. The typechecker it produces is actually an object, which gives you a runtime-inspectable type definition you can do more advanced stuff with. This also means prettier errors that look a lot like Luau type errors.

Examples

Easy Mode

local gt = require(path.to.GreenTea)

local stringType = gt.build(gt.string())

-- equivalent to takesString(value: string)
local function takesString(value: typeof(stringType.type()))
	-- will error if value is not a string
	stringType:assert(value)

	-- ...
end

Using gt is recommended, because GreenTea is a lot to type and gt is not!

How it Works

local stringTypeRaw = gt.string()

-- stringTypeRaw is typed in Luau as `string`
-- but at runtime, it's really a GreenTea.Type object.
-- This makes it easy to split it up into a runtime
-- typechecker and a Luau type:

-- give a name to the type
type stringTypeLuau = typeof(stringTypeRaw.type())

-- cast the object to the runtime typechecker type it really is
local stringTypeChecker = gt.typecast(stringTypeRaw)

-- You can think of this like "tricking" Luau into
-- "giving" us a `string` type.
-- This is all `GreenTea.build` does -- it typecasts
-- the value into a GreenTea.Type object, but includes
-- the original type os you can use `typeof`.

-- But the magic is this also works for more complex types!

local catTypeRaw = gt.table({
	age = gt.number(),
	meowSound = gt.string(),
	breed = {
		[gt.string()] = gt.number(),
	},
})

type catTypeLuau = typeof(catTypeRaw)
-- which is the same as...
type catTypeSame = {
	age: number,
	meowSound: string,
	breed: { [string]: number },
}

-- and we can get a runtime typechecker...
local catTypeChecker = gt.typecast(catTypeRaw)

-- OR we can make it really easy on ourselves if we just use `build` from the beginning:

local catType = gt.build(gt.table{
	age = gt.number(),
	meowSound = gt.string(),
	breed = {
		[gt.indexer(gt.string())] = gt.number(),
	},
}))

-- catType == catTypeChecker
-- catTypeLuau == typeof(catTypeChecker.type())

Using build is recommended because it makes this mostly type-safe:

  • The returned value is properly typed a GreenTea Type object, so you have typechecked access to GreenTea.Type's properties and methods.
  • TypeObject.type() can still be passed to GreenTea constructors to pass the GreenTea type in.
  • build runs GreenTea.typeof internally, so you can pass it simple tables and they'll be converted to the proper GreenTea.table type.

In this way, "built" types are for direct use, and "unbuilt" types are for composition with other GreenTea constructors.

Taking in GreenTea types

GreenTea is great for libraries that want to do typechecking, like RemoteEvent wrappers, because you can make the API very ergonomic:

-- RemoteWrapper.luau

function RemoteWrapper.new<T>(greenTeaType: T): RemoteWrapper<T>
	local self = {
		typechecker = GreenTea.build(greenTeaType),
		...
	}

	return setmetatable(self, RemoteWrapper)
end

function RemoteWrapper.onServerEvent<T>(self: RemoteWrapper<T>, fn: (player: Player, params: T) -> ())
	self.event:OnServerEvent(function(player: Player, paramsMaybe: any?)
		local params = self.typechecker:assert(params)
		fn(player, params)
	end)
end
-- CatSpawner.luau

local SpawnCat = RemoteWrapper.new({
	cat = {
		age = gt.number(),
		meowSound = gt.string(),
		...
	},
	location = gt.CFrame(),
	...
})

SpawnVehicle:onServerEvent(function(player, params)
	-- params is properly typed as { cat: { age: number, ...}, location: CFrame, ... }
end)

t Compatibility

GreenTea.t includes all of the methods from the t package. See the t package's readme for more details.

As a drop-in replacement

You can include the greentea-t-standalone package as an easy, drop-in replacement for a codebase which already uses t. Just add it to your wally.toml:

t = "corecii/greentea-t-standalone@0.4.7"

This package just exports GreenTea.t to make drop-in replacement easy.

Inspiration

This library was largely inspired by t, but also my experience working with t types, working with Luau types, and working with systems that need inspectable type definitions. This is my first attempt at solving all of these problems. I'd really like to see the inversion of this library -- where inspectable runtime type definitions are built from Luau types. But that's more work, so I'll leave it as a TODO!

Future Plans

  • Split up GreenTea.luau into multiple files. It's too long. But I'm tired and want to move on and actually use this library for cool things.
  • Stop lying to the typechecker once it's able to handle all of GreenTea's features.
  • Inverted GreenTea, where runtime typecheckers are built from Luau types.

About

A runtime typechecker with Luau types, pretty errors, and inspection

License:MIT License