FIx require tracing

This wasn’t submitted as an RFC since require is a Roblox-specific global. (yes require doesn’t exist in vanilla Luau)

The Problem:

As a Roblox developer it is currently very difficult to create relational references to modules, because require uses magic that forces an exact path in the call.

There are a few boilerplate fixes, my current one is

fancyRequire(script.Parent.Module :: typeof(require(script.Parent.Module)))

… where fancyRequire is <T>(T): T

This doesn’t work yay.

However, this currently raises a type error, because obviously you cant cast Instance to unknown. The problem here is pretty obvious

Problem 1: Library loaders

Lets assume you want to make a function that loads a module in a special way, this would obviously require a function.

local function tryRequire(mod)
  local s, e = pcall(require, mod)
  if s then return e end
end

This code wont typecheck any modules passed into it, all returns will be asserted as any, since the magic of require gets lost as soon as it gets a function argument passed to it.

Problem 2: Loss of information in lists etc.

Again, the same problem arises, ModuleScripts lose their magic as soon as you put them in a list, see image:

The issue also arrises with string keys.

The Solution.

A new Module<T> type to replace ModuleScript. (or even just use ModuleScript?), this would hold the magic return value as T, and then require can take it even if it’s given to a list, function argument etc.

The theoretical type of require would become require<T>(module: Module<T>): T

If Roblox were to address this issue it would improve my development experience since I wouldn’t need to rely on hacky types and type coercians to fix issues with the require function.

11 Likes

While I won’t argue the usefulness of this, I don’t think what you’re trying to achieve is smart. Why can’t you just keep the modules’ returns in a table?

local Library = {
	Math = require(script.Math),
	String = require(script.String),
	Table = require(script.String)
}

return Library
1 Like

The issue is mainly to do with library loaders (functions that use require internally, but do other stuff)

For example, lets assume I want to do something basic, require a module using pcall, should be simple enough right?

local s, e = pcall(require, script.Parent.catModule)
print(e) --> e = any

Support. Ran into an issue a few days back where I wanted to require all modules inside a script, however the require function only supports paths to ModuleScripts, not ModuleScripts themselves.

This means that the following code produces a type error:

for _, module: Instance in ipairs(script:GetChildren()) do
	require(module :: ModuleScript) --Error: Unknown require: unsupported path
	--Solution: require(module :: any) --No type error, but still an annoying workaround
	-- for something that shouldn't be an issue in the first place
end

It would be nice if the (currently somewhat rudimentary) require system could recognize ModuleScripts as a valid parameter type, as well as other improvements like this one.

PLEASE FOR THE LOVE OF GOD ADD THIS

As a person who likes to keep their scripts organized, I use my own custom framework which divides the Client and Server into functions, then each of those holds a table of modules and assets. I noticed that when I do for example: require(Framework:GetEngineServer().Packages.MyPackage) (where Server.Packages is typeof(Framework.Packages.Server)) just any is returned which is really annoying as I like to use autocomplete a lot. You could say that I could just require each module in the table, but that’s just a waste of time when you have lots of them, especially since this is an open source framework. Just this would solve 99% of the reasons why I opt out on using tables with modules.

Update:

@majdTRM has found a way to implement string literals in to functions (kind of). Here is the type (on my end):

export type Service = 
	(("DataService") -> typeof(require(InternalPackages.DataService)))
	& (("NetworkingService") -> typeof(require(InternalPackages.NetworkingService)))

Script:

local module = { }
local services
local Types = require(script.Types)

local GetService: Types.Service = function(service: string)
	return services[service]
end

module.GetService = GetService
1 Like