Problem with OOP type checking and cyclic module dependency

So, i have decided to use type checking with my OOP structure and i run into a problem where i have to make modules require each other to get their exported types.

I use custom Player and Team objects with their types written based on this tutorial:

Player object has a ‘Team’ property that uses the custom TeamTable type.

type self = {
	Name: string,
	Instance: Player,
	Character: Model,
	Team: Team.TeamTable --RIGHT HERE
}
export type PlayerTable = typeof(setmetatable({} :: self, Player))

And the Team object uses the custom PlayerTable type in it’s functions.

                                --RIGHT HERE
function Team:CanJoin(Player: Player.PlayerTable, JoinPrivilage: boolean?) : boolean

Unfortunately this leads to the fact that these modules need to require each other which creates the cyclic module dependency.

Team module
image

Player module
image

I know how to let modules require each other since i’ve done it before.
Basicaly this:

This solution will not let me get the types, as shown here:
image
It also gets rid of any autocomplietion that i prepared in the module with type checking.

Does anybody know what the solution could be? How could i store these types differently?

2 Likes

Create a third module that manages both of them. I don’t know, I’m not very experienced with types.

1 Like

So you know, this is intended behaviour because types are done statically, this behaviour won’t be changed.

You can get around the warning by casting your require call to any (local module = require(path.to.module) :: any but you lose type checking.

If you need types, you could define and import types in a third module as mentioned above this post

-- Types module

export type TeamTable = {
	-- ...
}

export type self = {
	Name: string;
	Instance: Player;
	Character: Model;
	Team: TeamTable
}

export type Player = { -- assuming this is your class methods table
	SetTeam: (self: PlayerTable, team: TeamTable) -> ();
	SomeOtherMethod: (self: PlayerTable, number) -> (boolean, number);
	__index: Player; -- important to inherit methods from the Player type
}

export type PlayerTable = typeof(setmetatable({} :: self, {} :: Player))

-- return a number to allow this module to be considered valid

-- tester:
local function a(player: PlayerTable)
	player:SetTeam({} :: TeamTable) -- ok
	player.Name = 'bob'

	player:ThisMethodDoesntExist({} :: TeamTable) -- not ok

	player.Instance = Instance.new('Fire') -- not ok
end

return 1 -- return 1 to allow this module to be considered valid

Then in your script,

local types = require(script.Types)

local function doSomethingWithAPlayer(player: types.PlayerTable)
	player:SetTeam({} :: types.TeamTable) -- again, ok
	player.Name = 'bob' -- ok
	
	player:nonexistentMethod() -- not ok
	player.Character = Instance.new('Fire') -- not ok
end
1 Like

Cyclic module dependency is valid here, because these modules are literaly requiring each other which causes the infinite loop.

As for the solution, creating a special module with copied types will only generate more unnecessary work, since i will have to update these types when i edit the modules.

Combining the modules is also not plausible since they are used individualy by other modules.

The real problem here is the inability to get types without requiring modules, since the Team module uses the Player module only for easier autocomplietion from its type.

I’ve made a temporary, empty replacement type in the Player module for the team property and i will only lose autocomplietion in minor scenarios.

type self = {
	Name: string,
	Instance: Player,
	UserId: number,
	Character: Character.Character?,
	Team: Types.TemporaryObjectType? --HERE
}
export type PlayerTable = typeof(setmetatable({} :: self, Player))

The real solution for this problem would be to add GLOBAL TYPES which can be accessed anywhere without needing to require modules, example:

global type PlayerTable = typeof(setmetatable({} :: self, Player))
--BOOM, NOW I CAN EASLY USE MY PLAYER TYPE ANYWHERE WITHOUT MODULE REQUIRING NIGHTMARE

I would make a feature request, but im unable to post any topics there.

You can make an issue on the Luau github detailing your usecases and why the addition is necessary. Typically in those cases you get a much faster response since it goes directly to the Luau team instead of having to go through devforum and having the staff here pass the message along to the relevant team

1 Like