Status: Implemented
Restrict generic type aliases to only be able to refer to the exact same instantiation of the generic that’s being declared.
Luau supports recursive type aliases, but with an important restriction: users can declare functions of recursive types, such as:
type Tree<a> = { data: a, children: {Tree<a>} }
but not recursive type functions, such as:
type Weird<a> = { data: a, children: Weird<{a}> }
If types such as Weird
were allowed, they would have infinite unfoldings for example:
Weird<number> = { data: number, children: Weird<{number}> }`
Weird<{number}> = { data: {number}, children: Weird<> }
Weird<> = { data: , children: Weird<}> }
...
Currently Luau has this restriction, but does not enforce it, and instead produces unexpected types, which can result in free types leaking into the module exports.
To enforce the restriction that recursive types aliases produce functions of
recursive types, we require that in any recursive type alias defining T<gs>
,
in any recursive use of T<Us>
, we have that gs
and Us
are equal.
This allows types such as:
type Tree<a> = { data: a, children: {Tree<a>} }
but not:
type Weird<a> = { data: a, children: Weird<{a}> }
since in the recursive use a
is not equal to {a}
.
This restriction applies to mutually recursive types too.
This restriction bans some type declarations which do not produce infinite unfoldings, such as:
type WeirdButFinite<a> = { data: a, children: WeirdButFinite<number> }
This restriction is stricter than TypeScript, which allows programs such as:
interface Foo<a> { x: Foo<a[]>[]; y: a; }
let x: Foo<number> = { x: [], y: 37 }
We could adopt a solution more like TypeScript’s, which is to lazily rather than eagerly instantiate types.