New Type Solver: Issues with Generics and Overloading

I was experimenting with the Type Solver after “require-by-string” got removed (I was using this feature a lot) while trying to make my Package Manager actually useful for the ones using it as well (not just the modules inside). All bugs in this report are related to the New Type Solver itself and thus I am not going to provide any links to games nor system information. If it helps, enabled Beta Features are listed below.

Enabled Beta Features

Assistant Mesh Generation, Avatar Joint Upgrade, Dragger QoL Improvements, Haptic Effects, Import Queue, Improved Constraint Tool, Incremental typechecking and autocompletion in studio, Multilayer Wrap Fix, New Luau type solver, New Studio Camera Controls, Next Gen Explorer, Next Gen Studio Preview, Preferred Text Size Settings, Revamped Asset Manager, Studio solid modelling improvements, Testure Generator, UI Drag Detectors, Unified Lighting, Video Uploads (Why is there not a “generate list of names” :sob:)

In order to see the behaviour yourself, check out BugReport - Type Solver.rbxl (59.6 KB). I tried to describe everything directly inside of the Scripts but I will list it here as well (for people who do not wish to download the place).

With some stuff I am not entirely sure whether they are bugs or not. I thought all was just “how it works” before my friend told me certain things mentioned might be bugs due to the stuff working differently in TS. Still, TS is not luau and it is possible some of the stuff is intented to work as they work.

First case - Problems with Generics
type modules = {
	RealTime: {
		setTime: () -> ()
	},
	Logs: {
		new: () -> ()
	}
}

type getter = <name>(name: name | keyof<modules>) -> index<modules, name>

local GetModule = function() end :: getter
GetModule("") -- ✅ Type Solver gives hints when one starts typing inside the string
local RealTime = GetModule("RealTime") -- ⚠️ Return type: index<modules, name> = not resolved, even though it matches the hint
Real -- (the "resolved" type can be seen by starting to type the rest of the variable name)

Type Checker resolved the following code alright BUT it is not really practical to use (not only due to no autocomplete for the singleton :: "RealTime")

local RealTime_fixed = GetModule("RealTime" :: "RealTime")

image

This (missing autocomplete) happens due to having the generic as another option to the “name” param (at least that is what I think). That leaves the option to put, in fact, any variable in the function and thus regular string is not converted to a singleton.
It would really help to be able to specify the type the generic variable can accept, something like:

type getter = <name: keyof<modules>>(name: name) -> index<modules, name>

Sadly this is not possible as of right now.

Even though this behavour is expected in a way, I consider this a bug since there is no way of avoiding manual type refinement. (Even type functions won’t help as there is no way to turn string into a singleton (as I was told) which … makes sense.)

Another option would be this:

type getter2 = (name: keyof<modules>) -> any

local GetModule2 = function() end :: getter2
GetModule2("") -- ✅ Type Solver gives hints when one starts typing inside the string and the hints are converted to singletons (I think)

One looses the ability to set dynamic return type though.

Second case - Problems with Generics with dynamic params

I wanted to give my Package Manager the ability to handle versions as well though, so let’s imagine the following simplified scenario:

type modules = {
	RealTime: {
		["1.0.0"]: {
			old: true
		},
		["2.0.0"]: {
			new: true
		}
	},
	Logs: {
		["1.0.0"]: {
			old: true
		},
		["1.0.1"]: {
			new: true
		}
	}
}

type versions = {
	RealTime: "1.0.0" | "2.0.0",
	Logs: "1.0.0" | "1.0.1"
}

type getter = <n>(name: n, verion: keyof<index<versions, n>>) -> any

For this use case I gave up on trying to have hints for the name, having to manually retype the string with every module get call as well as giving up the option to return the module itself …

local GetModule = function() end :: getter
GetModule("RealTime" :: "RealTime", )

image

As you can see here, the second argument is not resolved either. This is probably intentional, having no way to detect the arguments among each other dynamically, but I wanted to provide Developers at least the options of the available versions. If they want to use a module they probably already know the name of it but the versions data would be helpful.

Not sure if being able to specify the generic type for this would help the case though (something like this:)

type getter = <n: keyof<versions>, v: keyof<index<versions, n>>>(name: n, verion: v) -> index<index<modules, n>, v>

If I wanted the Developer to be able to at least use the package values he wants to use, I would have to do this:

type getter2 = <n, v>(name: n, verion: v) -> index<index<modules, n>, v>

local GetModule = function() end :: getter2
local module = GetModule("RealTime" :: "RealTime", "2.0.0" :: "2.0.0")

which resolves the type at the next use of “module” variable (not even at the end of the GetModule() call - definitely a bug)
This still comes at the cost of not having any hints AND having to retype everything manually.

image

Third case - Problems with Overloading using multiple params

I was ready to give up but luckily (I guess depends on who is the lucky one here) I remembered function overloading exists. So I went to it and tried them.
:information_source: All the following getters are, in fact, constructed by a type function dynamically from a list of modules. I am putting here the simplified results of those.

type getter = ((name: "RealTime", version: "1.0.0") -> "@Module[RealTime@1.0.0]")
& ((name: "RealTime", version: "2.0.0") -> "@Module[RealTime@2.0.0]")
& ((name: "Logs", version: "1.0.0") -> "@Module[Logs@1.0.0]")
& ((name: "Logs", version: "1.0.1") -> "@Module[Logs@1.0.1]")

local GetModule = function(name, version) end :: getter

Sounds nice, doesn’t it? (I bet it won’t be that nice when there are going to be a few more modules but … whatever :D ) One gets autocomplete as well! But try to complete the following get call, what do you see during typing?

GetModule("RealTime", "1.")


Also my friend noted here that if the params are merged (unioned), “1.0.0” shouldn’t be there two times (on the right).

That is right, version “1.0.1”. Where did it get from?!

Well, I assume that all the params of the same name (actually the name is irrelevant, it is the position of the param as I will show later) are merged together which … makes sense. Otherwise the autocomplete for the names would not work. But my friend (that is using TS for Roblox projects) pointed out something …

local module = GetModule("RealTime", "1.0.1")

[NEVERMIND] … this should error. In TS, he got 5-line long error starting with “No overload matches this call.”
▀▀▀▀▀▀▀▀▀ ► I forgot to put “–!strict” in the script, my fault. But one still has to retype the values ._.
Take a look at the return type as well but I will get to that in the last example.

The Type Error when --!strict present (technically I did nothing wrong as normal user would not understand that he gave the function a string but it wants a singleton).

The description is too long to fit in here so I will send the rest as a reply. I would like to shorten it or write down some kind of summary of the problems so it is easier to get the “important” stuff but (after 4 hours of getting this together) I have no idea what is important and what is not; scared of cutting off the important stuff and adding the unimportant ones.

Expected behavior

I am trying to make a function that returns module’s body based on a string input which is the module’s name. My goal is the function to support module names hinting while being able to return the right module body without the excessive need to retype the name of the module to a singleton in each function call. (GetModule("RealTime") rather than GetModule("RealTime" :: "RealTime"))

Ideally, generic types would support their own allowed type (something like T extends number in TS, for example) and type functions would be able to construct functions with param names as well. If nothing, Type Solver could at least convert strings to singletons if only singletons are allowed as the argument (which even though we cannot do in type functions directly, Type Solver is still capable of doing (otherwise GetModule2("RealTime@3.0.0").setTime() in the fourth case would not be possible, for example)).

1 Like

I feel bad for abusing the limit like this, sorry :sob:

Fourth case - Problems with Overloading using only one param

So here I am, disappointed at the turn outs of my tryings. Finally, after 3 days of intense research, I was close but not at the end at all. Then I realised something … what if I merge the names and versions?

type getter = ((name: "RealTime@1.0.0") -> "@Module[RealTime@1.0.0]")
& ((name: "RealTime@2.0.0") -> "@Module[RealTime@2.0.0]")
& ((name: "Logs@1.0.0") -> "@Module[Logs@1.0.0]")
& ((name: "Logs@1.0.1") -> "@Module[Logs@1.0.1]")

local GetModule = function() end :: getter

This has to work, this HAS TO WORK! Well … it does but actually does not.

So yeah, now there is no way to get false hints about the names and versions, all works perfectly …

GetModule("RealTime@1.0.0") -- is "@Module[RealTime@1.0.0]"

… or does it?

Just as I thought I won, I had to see something very suspicious. Let’s add one more module with actual data.

local GetModule2 = function() end :: getter & ((name: "RealTime@3.0.0") -> { new: true, setTime: () -> () })

So when I try to get the module, I am able to use the functions as well:

GetModule2("RealTime@3.0.0") -- returns the module
GetModule2("RealTime@3.0.0").setTime() -- function exists

in--!strict mode:

But let’s see what happens when I store the value:

local module = GetModule2("RealTime@3.0.0")
module -- is "@Module[RealTime@1.0.0]" now

Why? Why does it always return the first value no matter what you put there?

[EDIT] I was typing all this without the “–!strict” command (because I forgot to put it in the code I was testing this in as well), with the “–!strict” command present I, in fact, see an error that the argument does not match (because string is not equal to singleton … which it does not show lol) BUT still, I am able to access the content of the RealTime@3.0.0 module right after the call yet I am not able to acces it once stored in a value?

You see, even if I put the RealTime@3.0.0 first and then put something that is not even in the overloading

local GetModule3 = function() end :: ((name: "RealTime@3.0.0") -> { new: true, setTime: () -> () }) & getter
GetModule3("RealTime@3.0.2").setTime()

I am still able to access the .setTime() function as the first type is returned.

In fact, before writing this and realising my mistake with “–!strict” missing, I wanted to use this join method to help Developers to understand what they are typing. Because in fact, type functions are not capable of setting names for the params, thus the function in fact returns this:

type actualGetter = ((_: "RealTime@1.0.0") -> "@Module[RealTime@1.0.0]")
& ((_: "RealTime@2.0.0") -> "@Module[RealTime@2.0.0]")
& ((_: "Logs@1.0.0") -> "@Module[Logs@1.0.0]")
& ((_: "Logs@1.0.1") -> "@Module[Logs@1.0.1]")

And that is why I wanted to merge said functions with another one that has valid names for the params:

local confusingGetModule = function() end :: actualGetter
local betterGetModule = function() end :: ((name: nil) -> nil) & actualGetter

confusingGetModule("") -- "What am I typing?"
betterGetModule("") -- "Oh so they want the name!"

image
image

To make it the least confusing, I tried setting there something like this …

local theBestGetModule = function() end :: ((name: "*start typing*") -> "*module*") & actualGetter

theBestGetModule("") -- complete understandment

… but I was warned by my friend in advance that you guys would like to murder me for this :joy:

image

:information_source: Here is the right time to prove that actually names of the params do not matter. You see, the module names in the actualGetter have no names, yet when joined with (name: nil) -> nil in front of the whole actualGetter, all the names have “name” property name now.

Both betterGetModule() and theBestGetModule() leave me (or the other Devs that might use this in the future) with either “nil” or basically a string when saved to a variable and used later though.

local module2 = theBestGetModule("RealTime@2.0.0")
module2 -- is "*module*"
local module3 = betterGetModule("RealTime@2.0.0")
module3 -- nil

I would like to address two more things in the end:

  1. When typing the module names, the hint always shows for every second other character typed from the last start of typing (clicking in the string, deleting a char)
  2. When joining the getter with custom function types, the result does not have an icon of a function although it can still be called like one.