Type Annotation for Generic Function

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 the M to T, the type annotation shows it as expected, but in actuality T is not what I want; M is what I want because the purpose of a map function is to map the elements to something different; this may include a different type.

1 Like

Here’s a picture of the popping-up type sensing:

In this case, *error-type* should actually be Sequence<a>

1 Like

Bumping due to no responses after a day; still haven’t found a solution on my own

This happens because of recursive types. A type T<A> cannot be defined using T<B>.

Here is an example using iterators:

type Iterator<T> = {
--   ----------- T<A>
    next: (self: Iterator<T>) -> T,
--               ----------- Okay, still T<A>
    map: <U>(map_fn: (element: T) -> U) -> Iterator<U>
--                                         ~~~~~~~~~~~ Oh no! T<B>  
1 Like

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
        return n
    :map(function(index: number, element: number)
        return if index % 2 == 0 then "Even index" else element

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(
        {} :: {
            elements: {T}

-- The map function you described
function Sequence.map<T, U>(self: Sequence<T>, map_fn: (index: number, element: T) -> U): Sequence<U>
    -- ...

-- Another function, just to demonstrate.
function Sequence.as_tuple<T, U>(self: Sequence<T>): ...T
    -- ...

I personally prefer the first solution. The second one feels a bit “hacky” to me, like it can stop working at any moment.

1 Like

Awesome! I appreciate your time and effort in helping me to get this working. Cheers! :happy3:

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.