New Type Solver [Beta]

Directly from the announcement you are replying to:

Thank you for the clarification! I see that the keyof function is useful for extracting keys from tables with specific keys and values. However, I was referring to the possibility of having the ability to extract keys from tables like {"Default", "Foo"} directly, similar to how we work with Enums in Roblox, where we don’t need to specify the values manually but still can access the keys directly like Some.Default and Some.Foo.

Being able to use this pattern would make creating custom Enums easier without needing to define values for each name in the table, and it would allow us to directly access these keys in code. This would allow more streamlined and intuitive handling of Enum-like structures in Roblox.

I’m not sure I can reasonably envision the code you have in mind here. The recommended ways that people construct enumerations today for Luau are by defining a union type of the corresponding string singletons like so:

type MyEnum =
    | "VariantA"
    | "VariantB"
    | "VariantC" 

You’d then use this enumeration by just writing the string literal "VariantA", "VariantB", etc. in places, and autocomplete will work with the MyEnum annotation on functions and so forth to let you fill those string literals.

If you wanted to instead define this as an array as you’ve written, you could do so:

local MyEnum = { "VariantA", "VariantB", "VariantC" }

Currently, this will most likely get inferred with the type {string} (based on how string singleton type inference works), but even if it got { "VariantA" | "VariantB" | "VariantC" }, I’m not sure how you’d intend on using this table because you’d be writing MyEnum[1], MyEnum[2], etc. to actually get at the variants which doesn’t resemble Roblox enumerations at all.

For folks who actively want the usage of enums to look like Roblox’s builtin enumerations, the pattern I’ve seen people do is:

local MyEnum = {
    VariantA = "VariantA" :: "VariantA", -- the casts may not always be necessary
    VariantB = "VariantB" :: "VariantB", -- but they ensure that these will always
    VariantC = "VariantC" :: "VariantC", -- be typed as string singleton types
}

type MyEnum = keyof<typeof(MyEnum)>

since this lets you write MyEnum.VariantA to get a value of the appropriate enum. If you’re instead asking for some sort of syntactic sugar to make a definition like this less repetitive, that is certainly something that is possible, but it’s unrelated to the type system itself or TypeScript’s keyof that you mentioned, and would have to be proposed via Luau’s open source RFC process.

Can you elaborate on this? When you say better hashing, what do you mean?

Hello, I’ve discovered a weird bug with typing optionals inside a table, here’s the repro script and error message.

Type Error: Type '{ Model: Model }' could not be converted into 'MyCustomTable'; type { Model: Model }[read "Model"] (Model) is not exactly MyCustomTable[read "Model"][1] (nil)

--!strict
type MyCustomTable = {
	Model: Model?
}

local Foo: MyCustomTable = {
	Model = Instance.new("Model")
}

In comparison, this declaration causes no error:
local Model: Model? = Instance.new("Model")

This is unfortunately a known bug right now with expected types on table literal expressions. You can work around it by casting right now, i.e.

local Foo: MyCustomTable = {
	Model = Instance.new("Model") :: Model?
}

We’re working on overhauling the code responsible for this broadly though to resolve these kinds of bugs systematically as well as a number of related crashes that some users have been experiencing in the same codepaths.

3 Likes

Hello, me again with a more crtiical bug this time.

This snippet makes studio loop-crash (if its saved to the place) while the beta is active:

type Foo = {
	[string]: {
		Min: number,
		Max: number
	}
}
local Foos: Foo = {
	["Foo"] = {
		Min = -1,
		Max = 1
	},
	["Foo"] = {
		Min = -1,
		Max = 1
	}
}
1 Like

When using the next operator, it doesn’t recognize the types properly. Gives “K” and “V” where the old type resolver would give “string” and “number”:

local t: {[string]: number} = {
    Hello = 1,
    World = 2
}
for Name, Value in next, t do
    
end

Thank you for reporting this! We’ve got a fix in for this one, so you should expect to see this problem resolved in the Studio release next Wednesday.

2 Likes

found a bug with new type solver in strict mode:

--!strict
local Table = {}

local function RemoveKey(Key)
	Table[Key] = nil
end
local function AddKey()
	local Key = ""
	Table[Key] = 1 -- Type Error: Type 'number' could not be converted into 'nil'
end

some remarks:

  • error goes away if RemoveKey function is defined after AddKey
  • does not happen if doing Table["Key"] = 1, i.e. defining the index directly instead of through a variable.

Autocomplete gets completely disabled for any of a table’s functions if a table indexes itself inside of a function:

local Table = {
	a = 1
}

function Table.Foo()
	local Key = ""
	local Element = Table[Key]
end
function Table.Bar()

end

-- can no longer autocomplete Foo or Bar
-- can still autocomplete `a` however

minimal repro:

local Table = {}
function Table.Foo()
	local Key = ""
	local Element = Table[Key]
end
-- can no longer autocomplete Foo

this happens in both strict and non-strict mode.

Hello! I’m glad to see some good changes and bugs being fixed for the new type solver, but will there ever be an updated documentation with up-to-date definitions and examples of the types library?

I currently have 2 RFCs open and it’s not easy going through both of them at the same time, lol.

Another question, let’s say I have a function that takes in a dictionary as a parameter. And the output of this function is another table(proxy), but this time the places of certain keys and values have changed.

How would I use type functions to analyze the given dictionary’s key and value types and create a new table type accordingly? (Example: {variable_a: string, variable_b: number}{key_a: {variable_a: string}, key_b: {variable_b: number}})

2 Likes

Is there a way to hide some properties from showing up in the hint menu? I have decided to go for dynamic typing for my modules manager that is not actually that dynamic - by having a list of modules and their types. (Thanks so much for the keyof<> and index<> types!) The modules, however, have some properties for internal stuff and should not be able to be accessed from the outside of the module. These are the ones with “S3Si_” prefix:

In order to get rid of them, I tried to make an intersection which would set these to nil. As I have found out, intersections work the way that the first type overrides all same-named properties in the following ones, so I have done this:

type S3Si_default = {
	S3Si_modules: nil,
	S3Si_shared: nil,
	S3Si_config: nil,
	S3Si_init: nil,
	S3Si_load: nil
}

export type modules = {
	UserInput: S3Si_default & typeof(require("../Modules/UserInput").type)
}

However, this approach actually makes it even worse - because now there are all available properties with “S3Si_” prefix in the returned module, it makes it even more messy.


I have tried using unknown or never, too, but that didn’t change. However it wouldn’t make sense to hide these types anyways. (I thought never was an equivalent for void in C#, now I know what it trully means.) Since it is common to hide nil values in other languages, I consider this a bug (unless there is a way to do it).

Also, I think I have read it somewhere already but couldn’t find it now, the new type solver still does not recognise singletons (literals) - see the :GetModule() function call. The type of the function is the following, where types is the module containing the “modules” type above: GetModule: <module>(self: S3Si_modules, name: module) -> index<types.modules, module>

Why do you need these as hidden properties? What are you trying to do?

The manager does something with these properties but they are useless outside of it.
The S3Si_init and S3Si_load functions are executed by the manager (user should not be able to call these). The rest have some values and functionality assigned by the manager for the library/module itself to use. For example, S3Si_modules holds the manager itself so the libraries/modules can still use :GetModule() without circularly requiring the manager. S3Si_config leads to the library’s/module’s configuration Instance which the user should also not be able to access. In fact, all these properties are locked with a metatable and trying to access them returns a warning. And because they are useless to the user using the library/module, why should he see them?


Type solver fails to recognize type when returning behavior on a function. I am currently struggling to figure out to return the complete type behavior in my struct factory function. It appears that the language does not know how to handle dynamic dispatching of behavior.

Also how does Type functions work?

Out of curiosity, when will there be a new type solver update? I am facing so many issues right now with the type solver, it is giving me a head ache. The strange orange bar, generics not correctly being inferred, literally losing all autocompletion (yes, all of it)… It’s been too long since we got an update.

1 Like

The New Type Solver is being updated continuously, every single week, by the Luau team. You can find week-to-week descriptions of the updates in Roblox’s release notes. The only interruption to that since September was our holiday break! We tried to contextualize this in the initial announcement, but the New Type Solver is a sophisticated piece of technology with a ton of moving parts, and while we worked hard to get it into shape to launch the beta, it was never going to be feasible for us to replicate the wildly disparate uses of the type system in the wild by ourselves.

This does mean that our beta does not resemble the typical Roblox beta: this is not a totally finished product that we expect to release soon after the announcement. It’s a continuous effort to iterate on a complex piece of technology with the help of our more adventurous users, and so nobody should feel bad if they decide they’re not in that cohort and they would like to stick to the stability of the old type solver. We will keep you all posted on big changes to the beta as we work towards getting it ready for a general release.

If you’re experiencing specific problems and would like to see them get resolved faster, submitting a bug report that notes it is for the New Type Solver with a clear reproduction, a clear English description of the unexpected behavior, and a clear description of the expected behavior on either Luau’s open source issue tracker or the DevForum bug reports forum helps us considerably in working through issues.

2 Likes
--!strict
local Players = game:GetService("Players")
local player = Players.LocalPlayer -- (Value of type 'Player?' could be nil)

:pleading_face:

1 Like