Luau's New Type Solver: How to utilize type functions!

(:warning: Currently unfinished, but can still be read. Feedback is appreciated.)

:wave: Hello! Today, I’m here to teach you the basics on how to use the New Type Solver and its new features to advance your coding to the next level. Especially with the new built-in and custom type-function features.

What are Type Functions?


Type functions are functions that run during analysis time, and they operate on types instead of runtime values. Basically, they are functions that can transform types and even return new types, depending on what you want to accomplish. Type functions allow you to do achieve things that were not possible to do in the old-solver, such as inheritance, overloading, and more.


Built-in type functions


There are certain built-in type functions that you can already use to manipulate types, such as, keyof, rawkeyof.

keyof<T>

This type function returns an union type out of the keys of the given table type.
For example:

local animals = {
    cat = { speak = function() print "meow" end },
    dog = { speak = function() print "woof woof" end },
    monkey = { speak = function() print "oo oo" end },
    fox = { speak = function() print "gekk gekk" end }
}

type AnimalType = keyof<typeof(animals)> -- "cat" | "dog" | "monkey" | "fox"

rawkeyof<T>

Works the same as keyof, except it ignores metatables.
For example:

local tbl = setmetatable({test = ""}, {__index = {test_2 = ""}})

type a = keyof<typeof(tbl)> -- "test" | "test_2"
type b = rawkeyof<typeof(tbl)> -- "test"

index<T, U>

This function returns an union type out of the values of the given table type. Unlike keyof and rawkeyof, this function requires 2 arguments: A table and a key type.

For example:

type Person = {
	age: number,
	name: string,
	alive: boolean
}

type idxType = index<Person, keyof<Person>> -- number | string | boolean
type idxType2 = index<Person, "age" | "name"> -- number | string

Here, we gave the first argument as the Person table type to the index function. The first argument is the table that we are going to look up the types of values from. The second argument is the key type(s) that the lookup will be made with. This returns the type(s) of values found as an union.

rawget<T, U>

Works the same as index, except it ignores metatables.

setmetatable<T, U>

This type function works similarly to the main-runtime version of itself, except in the type-runtime, it creates a metatable type.

local clock = {}
type Identity = typeof(setmetatable({} :: { time: number }, { __index = clock }))

is now reduced to:

local clock = {}
type Identity = setmetatable<{ time: number }, { __index: typeof(clock) }> -- evaluates to { time: number, @metatable: { __index: typeof(clock) } }

getmetatable<T>

Like setmetatable, it works similarly to the main-runtime version of itself, except in the type-runtime, it returns the metatable type of the given type.

local animal = {}
type Identity = setmetatable<{ sound: string }, { __index: typeof(animal) }>
type ReversedIdentity = getmetatable<Identity> -- evaluates to { __index: typeof(animal) }

Custom Type Functions


So far, the built-in type functions already allow us to expand the capabilities of types, but we can take it to a whole new step.

We can create our own type functions that suit our needs, therefore we can use luau code itself to evaluate the logic behind how types are determined.

Here’s a basic type function example:

type function test()
	return types.string
end

type testType = test<> -- string

Here, we created a basic type function called test that returns a string type.
Just like the built-in type functions, we call custom type functions with < and > brackets.

:warning: Note: All type functions must return a type when called from outside of type functions. Otherwise, a type-error will be emitted.

(In addition to the types library, which is explained below, type functions also have access to these libraries and functions.)


The types library


In the previous example, to return a type, we used types, which is a global library which contains properties that reference built-in types, and functions that allows you to create your own types.

This library can only be used within type-functions, though it is very powerful.

The types library properties:

types.any
types.unknown
types.never
types.boolean
types.buffer
types.number
types.string
types.thread

(More info about the properties can be found here: Type checking - Luau)

Here’s an example type-function that replicates the built-in function keyof:

type function simple_keyof(ty: type)
    -- Ignoring unions or intersections of tables for simplicity.
    if not ty:is("table") then -- :is() is a method of the type object (given as the argument) similar to :isA of an Instance.
        error("Can only call keyof on tables.")
    end

    local union = nil

    for property in ty:properties() do -- :properties() is a method of a table type instance that allows you to retrieve properties of a table type.
        union = if union then types.unionof(union, property) else property -- .unionof is a function of the types library that creates an union type.
    end

    return if union then union else types.singleton(nil) -- .singleton is a function that creates a string literal (a.k.a singleton), such as "test".
end

type person = {
    name: string,
    age: number,
}
--- keys = "age" | "name"
type keys = simple_keyof<person>

Here, we replicated the functionality of the keyof built-in type function with our own custom type function. Our function takes in a table type, which gets represented as a type instance in the type-runtime.


The type instance


Types are represented as type instances in the type-runtime. This way, you can use them with luau code that allows you to change them or even create your own.

type instances can have extra properties and methods described depending on its tag.

type.tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "class" | "thread" | "buffer"

To see all methods that a type instance can have, you can visit this page: Type checking - Luau. In the next examples, I will mention some of the most popular ones.

Singleton type instance

For those who don’t know, singletons are types that represents one single value at runtime.
Strings and booleans are representable as singleton types, however, we generally use strings to represent singletons.

To create a singleton, you use the types.singleton() function.

types.singleton(arg: string | boolean | nil): type

Table type instance

Table type instances represent table types in the type runtime, and have certain special methods allowing you to transform them to whatever you wish.

To create a table type instance, you use the types.newtable() function.

Read and Write types

Table properties have separate types for reading from and writing to them. We call these types read and write types. The read type allows you to read from the property, whereas the write type allows you to write to the property. If a property has both of these types, it can both be read from and written into.

Read-only properties

Suppose you have a property called name, and you wish to indicate that this property can only be read from, and not be written on to. Using the new type-solver, you can specifically define this property to be read-only, using a special keyword called read. This operation removes the write type from the property, which makes it read-only.

type Person = {
	read name: string,
	age: number
}

local a: Person = {name = "", age = 18}
a.name = "" -- Type Error: Property name of the table 'Person' is read-only.

Write-only properties

The idea is the same as read-only properties, except this time we want to make a property write-only, this means it cannot be read from. Using the keyword write, you can make a property write-only. This operation removes the read type from the property, which makes it write-only.

type Person = {
	write name: string,
    age: number
}

local a: Person = {name = "", age = 18}
print(a.name) -- Type Error: Property name of table '{...}' is write-only.

setreadproperty(key: type, value: type?)

Sets the type for reading from the property named by key, leaving the type for writing this property as-is. The key parameter must be a singleton string type.

  • value will be set as the read type, the write type will be unchanged.
  • If key is not already present, only a read type will be set, making the property read-only.
  • If value is nil, the property is removed.

setwriteproperty(key: type, value: type?)

Sets the type for writing to the property named by key, leaving the type for reading this property as-is. The key parameter must be a singleton string type.

  • value will be set as the write type, the read type will be unchanged.
  • If key is not already present, only a write type will be set, making the property write-only.
  • If value is nil, the property is removed.

setproperty(key: type, value: type?)

Sets key-value pair in the table’s properties, with the same type for reading from and writing to the table. The key parameter must be a singleton string type.

type function test()
	local newTable = types.newtable() 
	newTable:setproperty(types.singleton("hi"), types.any)
	return newTable
end

type testType = test<> -- {hi: any}

In this example above, we created a new table type instance, using the types.newtable() function. This function creates a new type instance with the “table” tag, that comes with special methods that you can use. For example, :setproperty() being one of them, allows you to set the types of the properties within the table type. Using this, we set the key to the singleton string type "hi" using the types.singleton() function, and the value type of that key to any using the any property of the types library.

readproperty(key: type): type?

Returns the type used for reading values from this property, or nil if the property doesn’t exist.

type function customindex(tbl: type, key: type)
	if not tbl:is("table") then error("The first given type isn't a table!") end
	if not key:is("singleton") then error("The second given type isn't a singleton!") end
	
	local property = tbl:readproperty(key)
	if not property then error("Given property does not exist on the given table type.") end
	
	return property
end

type Person = {
	name: string,
	age: number
}

type indexResult = customindex<Person, "name"> -- string

In the example above, we created a basic custom index function (that does not create an union type) that returns the type of the found property, if it exists within the table type.
We first make sure to validate the types of the given types, tbl must be a table type, and key must be a singleton type. And then we use the :readproperty() method of the table type with the given singleton type to get the property, and return it.

This function only works on properties that have a read type. So, only properties that aren’t write-only will be returned.

writeproperty(key: type): type?

Returns the type used for writing values to this property, or nil if the property doesn’t exist.

This function works the same as to readproperty(), except it only returns properties that aren’t read-only, because it only works on properties that have a write type.

setindexer(index: type, result: type)

Sets the table’s indexer, using the same type for reads and writes.

Indexers are the default types that return the result type of an index operation on a table.

type function createTable()
	local newTable = types.newtable()
	newTable:setindexer(types.number, types.string)
	return newTable
end

type tableType = createTable<> -- {[number]: string}

In the example above, we created a new table type and set its indexer to {[number]: string}, which means when we index this table with a number, it will return a value of a string type.

(More methods of the table type instance can be found here: Type checking - Luau)

Function type instance

The function type instance represents function types in the type-runtime.

setparameters(head: {type}?, tail: type?)

Sets the function’s parameters, with the ordered parameters in head and the variadic tail in tail.

type function createFunction()
	local newFunction = types.newfunction()
	newFunction:setparameters({
		types.string,
		types.number
	})
	return newFunction
end

type functionType = createFunction<> -- (string, number) -> ()

In the example above, we created a new function type instance using the types.newfunction() function, and set its parameters by using :setparameters(). This function takes a head table, that has types that determine the types of the parameters of the function. Unfortunately, you cannot set the names of the parameters currently, and can only set the types in order.

You can also use the tail parameter to create a variadic parameter for the function type:

type function createFunction()
	local newFunction = types.newfunction()
	newFunction:setparameters({
		types.string,
		types.number
	}, types.boolean)
	return newFunction
end

type functionType = createFunction<> -- (string, number, ...boolean) -> ()

setreturns(head: {type}?, tail: type?)

Sets the function’s return types, with the ordered parameters in head and the variadic tail in tail.

type function createFunction()
	local newFunction = types.newfunction()
	newFunction:setparameters({
		types.string,
		types.number
	}, types.boolean)
	newFunction:setreturns({
		types.boolean,
		types.any
	})
	
	return newFunction
end

type functionType = createFunction<> -- (string, number, ...boolean) -> (boolean, any)

In the example above, we’ve added the return types of the function, using the :setreturns() function. Like :setparameters(), it takes a head table, and can also take a type in its tail parameter, to create a variadic return type.

setgenerics(generics: {type}?)

Sets the function’s generic types.

type function pass()
	local T = types.generic("T") -- T
	local Us, Vs = types.generic("U", true), types.generic("V", true) -- U..., V...

	local f = types.newfunction()
	f:setparameters({T}, Us); -- (T, U...)
	f:setreturns({T}, Vs); -- (T, V...)
	f:setgenerics({T, Us, Vs}); -- <T, U..., V...>
	return f
end

type X = pass<> -- <T, U..., V...>(T, U...) -> (T, V...)

In the example above, we created a new function type that contains generic types, and type packs.
To create a generic function type, we first create a generic type, by using the types.generic(name: string?, ispack: boolean?) function. This function can either create a generic type, or a generic type pack.

Using this function, we first created a generic called T, then we created 2 generic type packs called: U and V. Then we created a new function, and set its parameters to the T generic and the U generic type pack, and then set its returns to the T generic and the V generic type pack.

Finally, using the :setgenerics() method, we set the function’s generics to the T generic, and the U and V generic type packs, giving us the final function type that is set to the X type alias.

(More methods of the function type instance can be found here: Type checking - Luau)

Class type instance

This is a special type that cannot be created manually, as it represents classes that are defined outside of the Luau runtime. Instance for example, is a Roblox Engine Class that does not exist within the Luau runtime itself. These classes can only be defined in Luau definition files that only runtime embedders can use. But we can still operate on them.

properties(): { [type]: { read: type?, write: type? } }

Returns the properties of the class with their respective read and write types.

metatable(): type?

Returns the class’ metatable, or nil if it doesn’t exist.

classtype:indexer(): { index: type, readresult: type, writeresult: type }?

Returns the class’ indexer, or nil if it doesn’t exist.

(More methods of the class type instance can be found here: Type checking - Luau)

For more information about the types of the type instance, you can visit here: Type checking - Luau


General Examples and Places where you can use Custom Type Functions


Coming Soon


What is next?


Coming Soon


19 Likes

Is it possible to define an export type in Roblox Studio (Luau) that has different structures for server and client? For example, can I have
export type Some = { serverData: string, sharedData: number } on the server and
export type Some = { clientData: boolean, sharedData: number } on the client, while keeping them separate and type-safe?

Fr, those pesky things are so complex. Which is why they are awesome :smiley:

I know that you know this too, but the key type cannot be any, string, number, buffer, boolean, never, thread, unknown, union, intersection, negation, table, class, generic, or function. It can only be a singleton, with the indexer being one of the other types above. It can NOT be true, false, or nil either within the singleton space. Since we may get number singletons in a few years or decades, cannot exclude those too.

setproperty is offically documented with the key parameter being set to a type type. Yes, it can only take a singleton, I’ve mentioned that as well.

1 Like

Absolutely! Also, the type-runtime is seperated from the normal runtime. Type-runtime only works in an lsp or Studio. So don’t worry about server and client differences with types.

It depends on how you use them, if you use the type function solely for use in other type functions, it doesn’t matter the input or output types, can even use variadics. However, if you are to use those type functions like normal then they will error.

-- no use since .tag exists and :is(...) exists, but tis an example
type function getTag(a: type)
	return a.tag -- a string
end

type function isUnion(a: type)
	return getTag(a) == "union" and types.singleton(true)
		or types.singleton(false)
end

type Welcome = "Hello World"
type TagOfWelcome = getTag<Welcome> -- error, we would have to change getTag to return a singleton of the tag
type isWelcomeUnion = isUnion<Welcome> -- false

I will update it to be more clear later, thank you. But variadics are not supported within type functions yet. (In fact I made an RFC to allow for variadics support in type functions here: RFC: Support Variadic User-Defined Type Functions by TenebrisNoctua · Pull Request #117 · luau-lang/rfcs · GitHub)

I should have clarified that they are currently capable of being used only in the type functions that are used like normal functions and do not work when trying to use them like type functions.

type function getTags(...: type)
	local args = {...}
	local tag = args[1].tag
	args[1] = nil
	for _,v in args do
		if v.tag ~= tag then return "" end
	end
	return tag
end

type function isUnion(a: type, b: type, c: type)
	return getTags(a, b) == "union" and types.singleton(true)
		or types.singleton(false)
end

type WelcomeA = "Hello"?
type WelcomeB = "Howdy"?
type WelcomeC = "Hey"?

type isWelcomeUnion = isUnion<WelcomeA, WelcomeB, WelcomeC> -- true

-- Error: Generic type expects 0 arguments, got 3
type getTagWelcome = getTags<WelcomeA, WelcomeB, WelcomeC>

1 Like

Updated the tutorial:

  • Included Read-only and Write-only properties.
  • Included more table type functions and examples.
  • Clarified and re-sorted certain sections.

Upcoming:

  • Function type instances.
  • Class type instances.
  • More information about certain features.
  • Info about where you can efficiently utilize type functions.
  • What is next?

This post will go strait into my bookmarks


I’ve abstained from learning type functions, and working with the new type solver in general, well from a lack of time, but also because it is still very buggy
It does seem to be much better now than a couple of months ago, it doesn’t take years at least to start up, and the errors and warnings are less frequent, or more descriptive

I remember trying out type functions to give types to the arguments of functions passed to :Connect() (and others methods) in my signal library, by specifying types when creating the signal, however, I very much failed, it just seemed like type functions were broken. I remember trying an example on the luau.org type checking page, and it wasn’t working

image
I do see that this one is working. I’m assuming this is the one I tried out some time ago, as I don’t see other examples on the website

What would you say the state of the new type solver is? Is it usable, or is it still buggy? I also assume that these newer and more complex features are more prone to bugs, so are they working nicely for you, or are bugs still very common?

It’s still not in a production-ready version. Every week it gets better as more features and bug fixes are implemented, but still not there yet. It still has a couple months worth of progress and bug-fixing to reach to a point where it can safely replace the old-solver, without any real problems.

Though I do think it’s pretty usable at this point.

1 Like

Hey, another question, more in general though

I’ve always had the need to create types for methods, but never found a way to do so without using typeof() or the type inference engine,

And by methods, I mean I want autocomplete for functions, but by using the : notation

image
In this case it doesn’t know what self is, I thought maybe now it’s smart enough to understand with self, that it’s supposed to be a : method

Is there a way to do this without workarounds? I’m also not using this in the context of OOP, or if I do, it’s without metatables. I just prefer the : notation in general

You can already do this by declaring the self as the type of the table the function is in.
For example:

type test = {
   num: number,
   func: (self: test, number) -> ()
}

local a: test
a:func --> should have autocomplete
1 Like

Updated the tutorial:

  • Added function type instances.
  • Added class type instances.
  • Added more information about read and write types.
  • Added the built-in metatable type functions.
  • Clarified certain sections.

Like enforcing :? Noctua’s is the only approach I know of that doesn’t involve typeof and sadly it doesn’t enforce. I’ll continue this reply under the assumption that you are trying to enforce : through any means, even though I know you already have some typeof approaches in mind.

With type functions it isn’t possible currently since we cannot name parameters in functions, and would need to name the first parameter self. I am going to provide two approaches using typeof and an example using the bottommost one.

Classical use for enforcing without generics

--!strict
--local a = function(...) end -- for old solver
local b = {}
-- Comment goes here
function b:func(_: number)
	--if self and a(self) then end -- for old solver, new solver does weird things with this
end
type Test = {
	num: number,
	func: typeof(b.func)
}
local t: Test = nil :: any

Allows for generics to be used (probably a way to make it look better):

--!strict
type Test<Variant, A...> = {
	num: Variant,
	func: typeof(((nil :: any) :: typeof(
		function()
			--local a = function(...) end -- for old solver
			local b = {}
			-- Comment goes here
			function b:func(...: A...)
				--if self and a(self) then end -- for old solver, new solver does weird things with this
			end
			return b.func
		end
		))())
}

local a: Test<number, string, boolean> = nil :: any

A usage case that involves generics, there are two approaches, a manual generic typing approach and an automatic one. The manual is provided, but you have to adjust the code to work for the automatic, same goes for changing to the old solver.

Sample Signal Type (both old and new solver w/o type functions)
--!strict
-- Old Solver:
-- `local a= function(...) end` is needed
-- `if self and a(self) then end` is needed

-- New Solver:
-- `if self and a(self) then end` breaks things, so don't use it
-- by extension, `local a= function(...) end` is now useless

export type Connection = {
	Disconnect: typeof(((nil :: any) :: typeof(
		function()
			local a = function(...) end
			local b = {}
			--[[
				Disconnects the connection from the signal.
			]]
			function b:Disconnect()
				--if self and a(self) then end -- for old solver, new solver doesn't need this
			end
			return b.Disconnect
		end
	))())
}

export type Signal<T...> = {
	Connect: typeof(((nil :: any) :: typeof(
		function()
			local a = function(...) end
			local b = {}
			--[[
				Connects the given function to the signal, returning the corresponding Connection.
			]]
			function b:Connect(callback : (T...)->())
				--if self and a(self) then end  -- for old solver, new solver doesn't need this
			end
			return b.Connect
		end	
	))()),
	Once: typeof(((nil :: any) :: typeof(
		function()
			local a = function(...) end
			local b = {}
			--[[
				Connects the given function to the signal for a single invocation, returning the corresponding Connection.
			]]
			function b:Once(callback : (T...)->())
				--if self and a(self) then end  -- for old solver, new solver doesn't need this
			end
			return b.Once
		end	
	))()),
	Fire: typeof(((nil :: any) :: typeof(
		function()
			local a = function(...) end
			local b = {}
			--[[
				Invokes the functions connected to the signal.
			]]
			function b:Fire(...: T...)
				--if self and a(self) then end  -- for old solver, new solver doesn't need this
			end
			return b.Fire
		end	
	))()), --(self: Signal<T...>, T...)->(), 
	Wait: typeof(((nil :: any) :: typeof(
		function()
			local a = function(...) end
			local b = {}
			--[[
				Yields until the signal fires.
			]]
			function b:Wait()
				--if self and a(self) then end  -- for old solver, new solver doesn't need this
			end
			return b.Wait
		end	
	))()),
}

export type TSignal<T...> = { -- to TSignal
	new: typeof(
		--[[
			Returns a Signal object
			@kind Event
		]]
		function() : Signal<T...> -- to function<T...>() : Signal<T...>
			return (nil :: any) :: Signal<T...>
		end
	),
	DisconnectAll : typeof(
		--[[
			Disconnects all connections from the signal
		]]
		function() : ()
		end
	)
}

local a: TSignal<string, number>

-- Old Solver:
-- Change :Fire to the commented code to have that autogeneric typing thing
-- (self: Signal<T...>, T...)->(), -- <- this
-- change TSignal<T...> to be TSignal and make new become function<T...>
--local a: TSignal
--a.new():Fire("", "") -- string, string pops up

-- New Solver:
-- Change TSignal<T...> to be TSignal and make new become function<T...>
--local a: TSignal
--local b = a.new()
--b:Fire("", "") -- makes it Signal<string, string>

I don’t really care if the autocomplete still gives autocomplete for the . notation, as it’ll also tell to use self, which is correct

I’ve managed to type my signal module using @TenebrisNoctua answer, and some more searching (albeit it requires a manual Signal : SignalModule.Signal<number> to work, but I think there is no getting around that)

The types look like this

-- // Temporary types for the Disconnect signal

-- TODO - Currenty unable to define the DisconnectObject type as SignalType<> and EventType<> because of the current limiations of the typechecking engine regarding recursive types

type temp_Event = {	
	Connect : (self : temp_Event, Function : () -> ()) -> DisconnectObject,
	UnsafeConnect : (self : temp_Event, Function : () -> ()) -> DisconnectObject,
	Once : (self : temp_Event, Function : () -> ()) -> DisconnectObject,
	UnsafeOnce : (self : temp_Event, Function : () -> ()) -> DisconnectObject,
	Wait : (self : temp_Event) -> (),

	-- Private
	_ : {
		Signal : temp_Signal
	}
}

type temp_Signal = {
	Fire : (self : temp_Signal) -> (),
	DisconnectAll : (self : temp_Signal) -> (),
	Destroy : (self : temp_Signal) -> (),
	Event : temp_Event,
}

-- // Types

export type DisconnectObject = {
	Connected : boolean,
	Disconnect : (self : DisconnectObject) -> (),
	Disconnected : temp_Event,
	
	-- Private
	_ : {
		DisconnectedSignal : temp_Signal,
	}
}

export type Event<U...> = {	
	Connect : (self : Event<U...>, Function : (U...) -> ()) -> DisconnectObject,
	UnsafeConnect : (self : Event<U...>, Function : (U...) -> ()) -> DisconnectObject,
	Once : (self : Event<U...>, Function : (U...) -> ()) -> DisconnectObject,
	UnsafeOnce : (self : Event<U...>, Function : (U...) -> ()) -> DisconnectObject,
	Wait : (self : Event<U...>) -> U...,
	
	TagDisconnectObject : (self : Event<U...>, DisconnectObject, Tag : string) -> (),
	DisconnectTagged : (self : Event<U...>, Tag : string) -> (),
	
	-- Private
	_ : {
		Signal : Signal<U...>
	}
}

export type Signal<U...> = {
	Fire : (self : Signal<U...>, U...) -> (),
	DisconnectAll : (self : Signal<U...>) -> (),
	Destroy : (self : Signal<U...>) -> (),
	Event : Event<U...>,
}

You might notice that there are temporary types, for the DisconnectObject signal, and that is because using Event<> and Signal<> (for a signal that doesn’t have arguments) causes a type checking warning, “Type Error: Recursive Type Being used with Different Parameters”. From what I read, this is a limitation of the current type checking engine

1 Like

You can do this with type functions, it’s just that the first argument must have the same type as the first provided object, I do this within my own Class module to automatically create a self type parameter.

I’ll include it later when I update the tutorial for the last sections.

1 Like

Stand corrected. Although I do look forward to seeing your approach because the one I quickly crafted to verify isn’t capable of the typical self shenanigans.

makeMethod
type function makeMethod(a: type, b: type)
	-- a must be any, unknown, or a table, or a union with any of those included
	if not b:is("function") then
		print("Invalid type: The second input must be a function.")
		return b
	end
	local head = b:parameters().head
	if head then
		table.insert(head, 1, a)
	end
	
	return types.newfunction(
		{head=head, tail=b:parameters().tail}, 
		b:returns(), 
		b:generics()
	)
end

type B = {
	-- replacing any with B doesn't work :<
	a: makeMethod<any, (string)->()>
}
local b: B