Hello! I read the Luau type-checking documentation for generic functions:
However, I ran into an issue with the type sensing. Let’s say I have some custom Sequence type representative of an array and I want to create a map function that maps elements from the current sequence to a new sequence via some input transformer function with index and element parameters:
type Sequence<T> = {
elements: {T},
map: <M>(transformer: (index: number, element: T) -> M) -> Sequence<M>
}
For the most part, this works fine, however, the issue arises with the use of the generic M in the return type of the map function. It appropriately uses M in the return type of the transformer function, but the return type of the actual map function is *error-type*. Any ideas as to how I can resolve this issue?
EDIT: As clarification, if I change theMtoT, the type annotation shows it as expected, but in actualityTis not what I want;Mis what I want because the purpose of amapfunction is to map the elements to something different; this may include a different type.
Thank you for your response! Sorry I did not reply earlier; for some reason, I did not get a notification. I can see why you can’t define it like that because it would essentially result in infinite recursion, trying to define the type in every generic M. So since that is the case, is there any workaround or some other solution? It would be nice to be able to map it like that. Even using something like any or another fixed type like number does not work.
I know two ways off the top of my head to make this work.
You can separate data from logic:
-- The data inside a sequence
type SeqStruct<T> = {
elements: {T}
}
-- The functions (or methods) inside a sequence
type SeqImpl = {
-- The map function you described
map: <T, U>(
self: SeqStruct<T> & SeqImpl,
map_fn: (index: number, element: T) -> U
) -> SeqStruct<U> & SeqImpl,
-- Another function, just to demonstrate.
as_tuple: <T>(
self: SeqStruct<T> & SeqImpl
) -> ...T
}
-- Note that this is the actual public type (we used `export`)
-- This means other modules will never be able to separate `SeqImpl` and `SeqStruct`
export type Sequence<T> = SeqStruct<T> & SeqImpl
-- And here is an example using the system:
local sq_string: Sequence<string> = fn_that_returns_a_sequence_of_strings()
local a, b, c, d, e = sq_string
:map(function(index: number, element: string)
local n = tonumber(element)
if n == nil then
return 0
end
return n
end)
:map(function(index: number, element: number)
return if index % 2 == 0 then "Even index" else element
end)
:as_tuple()
print(a, b, c, d, e)
-- ^------------ These are automatically `number | string`
Or you can do this, which, to be honest, I’m not sure why it works, but it does:
local Sequence = {}
Sequence.__index = Sequence
type Sequence<T> = typeof(
setmetatable(
{} :: {
elements: {T}
},
Sequence
)
)
-- The map function you described
function Sequence.map<T, U>(self: Sequence<T>, map_fn: (index: number, element: T) -> U): Sequence<U>
-- ...
end
-- Another function, just to demonstrate.
function Sequence.as_tuple<T, U>(self: Sequence<T>): ...T
-- ...
end
I personally prefer the first solution. The second one feels a bit “hacky” to me, like it can stop working at any moment.