Custom Enumerations

Enumerations or Enums are a staple of programming and are incredibly useful when developers would like to use integers, floats, or some other data type, by name from a set of values. Currently, there is no way to create custom enums in roblox, and although they can be mimicked by using tables, this prevents the Enum from receiving syntax notes from Luau.

In this example, roblox Enums are powerful for type specification, as they can be chosen distinctly by their parent class.

--- Extract physical properties from a material
-- If a {basePart} is provided, will report its own physical properties
local function GetFriction(material: Enum.Material, basePart: BasePart?): PhysicalProperties
    -- TODO: body
end

And it would be useful to have a similar case for custom enumerations:

Envelope.LoopMode = LoopMode.PingPong;
-- LoopMode.Cyclic, LoopMode.Clamp, LoopMode.Repeat

With "Enum"s generated from Lua class / table factories, Luau will be incapable of providing those type annotations and warnings, and you cannot build a type dynamically to represent the Enum without doing something annoying and crazy like this (and even then I cannot get GetEnumItems to work)

It would be great to have some sort of interface to create Enums dynamically.
The only other way to be able to type enums is to be able to manually declare sealed table types (i.e. type CustomEnum<T> = typeof(setmetatable(<CustomEnumItem<T, CustomEnum<T>>{}, metatable)))

It would also be incredibly helpful if they could be placed where regular Enums can be, like in object Attributes and serialized over event calls.

49 Likes

I love this idea. I doubt they would add it though

2 Likes

I like this idea as well. When I started developing on the platform, I was looking for a way to create enumerated types (C/C++ has the enum keyword). However, I have resorted to just using tables to do it like so:

Enumerations.LUA

module = {
	-- Enumeration: NPC Action Type
	NPCActionType = {
		Kick = 0;
		Punch = 1;
		WeaponMelee = 2;
		WeaponShoot = 3;
		Spell = 4;
		Special = 5;
		Custom = 100;
	};
}

return module

Then to use it, I do this:

-- ******** Requirements

-- Required Game Services and Facilities
local playerService = game:GetService("Players")

-- Scripting Support
local childWaitTime = 10
local serverScriptService = game:GetService("ServerScriptService")
local coreLibs = serverScriptService:WaitForChild("Libraries", childWaitTime)
assert(coreLibs, "Core library folder failed to load.")

-- Required Libraries
local enums = require(coreLibs.Enumerations)

It does work, and if you need to loop over the enumerations, you can do this:

for name, value in pairs(enums.NPCActionType) do
	-- Do something with the enum.
	print(name, "=", value)
end

But I digress. Being able to create custom Enums is something that is sorely needed, even if it’s just adding the enum keyword.

2 Likes

The preferred way to do this is using literal string unions.

type Color = "red" | "green" | "blue"

local function paintObject(color: Color)
    -- etc
end

paintObject("red")
paintObject("orange") -- Type error

You can even combine these with other Luau tricks in order to exhaustive match.

if color == "red" then
    -- red path
elseif color == "green" then
    -- green path
else
    -- This typecast will fail if any path isn't hit
    local exhaustiveMatch: never = color
    error(`Unknown color: {exhaustiveMatch}`)
end
8 Likes

Although this is the approach I have been using as well, it leads to problems like discoverability and typing. There is also a larger use for enums, for example, with flags and states

class NPCState(IntFlag):
    DEFAULT = 0
    IN_COMBAT = auto()
    IS_INJURED = auto()
    IS_ALIVE = auto()
    HAS_TARGET = auto()

myNpc.state = NPCState.IS_ALIVE | NPCState.HAS_TARGET

if myNpc.state & NPCState.HAS_TARGET:
    # target chasing code

(C++ version)

enum class NPCState {
    DEFAULT = 0,
    IN_COMBAT = 1,
    IS_INJURED = 1 << 1,
    IS_ALIVE = 1 << 2,
    HAS_TARGET = 1 << 3
};

myNPC->state = NPCState::IS_ALIVE | NPCState::HAS_TARGET;

if (myNPC->state & NPCState::HAS_TARGET)
{
    // target chasing code
}

And enums being their own class / type makes it easier to discover and explicitly define enum types without having to use string literals. And there may be other cases where you would like to be able to distinguish from a string literal and an enum in a function, i.e. if using overloaded or variadic functions, or be able to iterate over enums and have the functionality of functions/properties like GetEnumItems(), enum.EnumType, enum.Name, and enum.Value.

While it is definitely possible to make a lua object to represent or copy the behavior of Enums and Flags, the typing system does not provide a way to be able to explicitly merge the keys of an initial table, so unless the “Enum” is a generic sealed table, typing will not provide any hints to the properties or objects of the enum.

CustomEnum.XY... -- will not perform type completion unless CustomEnum is an explicit sealed table
CustomEnum.XYZ.EnumType.X... -- ditto
for _, item in CustomEnum:GetEnumItems() do
    print(item.EnumType.X... -- ditto
end

if myEnum:IsA(CustomEnum) then --[[...]] end

There are also problems when refactoring, as it can be more complicated to refactor a string that could be reused for other purposes, vs an enum

1 Like

For discoverability, the autocomplete will automatically suggest the variants on a string union.

image

I understand your other use cases of things like GetEnumItems() equivalents but I think it’s unlikely we add much more than this as a first class feature.

3 Likes

Literal string unions don’t really do what enums do though
If you’re working with anything that doesn’t take strings (like APIs) then it’s not useful. Also IMO using strings for input is just kinda ugly.
I would really prefer to have a table of names and numbers (which is all an enum really is) than a union of a bunch of strings and while I can do that with just a normal table, I’d like proper integration with the checker and autocomplete
Please reconsider

4 Likes

You are the one building the APIs in this case–there’s no internal APIs that would take a custom enumerator that isn’t exposed in the engine.

A mapping of table to enum value is achievable with this approach, if you want to extract using raw strings:

type Color = "blue" | "red"

local Color = {
    blue = "blue" :: Color,
    red = "red" :: Color,
}
2 Likes

this is real

basically copying the entire enum thing and making enum equal to the copy, since this copy is not frozen well we can edit it

1 Like

Hey, quick question. Is there a way to automate adding new literal string unions?

For example with tables:

local table = {}
table.insert(table, "item")

-- { [1] = "item" }

Is there a way to do this with literal string unions?

type Color

and then add strings?

1 Like