Generic functions widen enums prematurely to give typechecking error

Reproduction Steps

Create a new place, copy/paste this script into it:

Source Code
--!strict

--Generic function which infers the first argument's type; this is the same type that gets returned.
local function PrintAndReturn<T>(value: T): T
	print(value);
	return value;
end

--Works for numbers & strings.
local myNumber: number = PrintAndReturn(5);
local myString: string = PrintAndReturn("foo");

--Does not work for enums (string intersections). The type I'm passing in is "foo"|"bar", but the
--engine is converting it to string|string.
type MyEnum = "foo"|"bar";
--[[

Type Error: (23,1) Type 'string | string' could not be converted into '"bar" | "foo"'
caused by:
  Not all union options are compatible. Type 'string' could not be converted into '"bar" | "foo"'; none of the union options are compatible
  
--]]
local myEnum1: MyEnum = PrintAndReturn("foo" :: MyEnum);

--I'd be sympathetic for it messing up here, since it's unclear whether "foo" is meant to be a
--special string constant or merely a string, hence why I don't object to requiring a cast to "foo"
--or MyEnum.
--
--That said, if it preserved itself as a string constant, it could be widened at any point in the
--future.
local myEnum2: MyEnum = PrintAndReturn("foo");

--Our good friend, the old "any" escape hatch.
local myEnum3: MyEnum = PrintAndReturn("foo") :: any;

Expected Behavior

I expect no type error, as my argument is of type “foo”|“bar”, and hence the return type should be as well.

Actual Behavior
There is a type error.


Workaround

Yeah, there’s always the escape hatch of casting to any.

Issue Area: Studio
Issue Type: Other
Impact: Moderate
Frequency: Sometimes

1 Like

Thanks for the report! We’ll follow up when we have an update for you.

2 Likes

I’m a little confused on what you’re trying to do here.

If I wanted to make a new string enum type here is what I would do:

...
type MyStringEnum = Enum
local myEnum : MyStringEnum = { "bar", "foo" }
local myEnum1: MyStringEnum = PrintAndReturn(myEnum["foo"] :: MyStringEnum);
-- or
local myEnum2: MyStringEnum = PrintAndReturn(myEnum["foo"]);
...

What I see you doing is implicitly create a union type but it’s a union of a string and string which then causes the type checker to throw an error because “string” is not the same as "string union string".

Maybe I’m misunderstanding something though…

I think maybe there’s some miscommunication in what I mean by “enum”.

I’m using it to mean a collection of valid values.

type ButtonState = "idle"|"hovered"|"clicked";

Just as a way to enforce (or lint, at the very least) that I don’t attempt to assign anything invalid into a variable of this type.

RobloxStudioBeta_ah3qJxZre9

I’m not using it in relation to the global variable “Enum” or any of its related types, as they can’t be used to accomplish this same goal. Actually, I’m messing around in studio & I can’t actually fathom what the built-in type “Enum” is supposed to be. It has no intellisense, and a table with any contents can be cast to it, but numbers and strings can’t. It also doesn’t have the GetEnumItems() method. Is it just an alias for {[any]: any}?


So yes, I’m trying to create a union type, but it’s a union of two specific strings.

Cool, we have a ticket for this and will follow up as mentioned above.