Skip to content

RFC: Name support for all Type Pack annotations#206

Open
karl-police wants to merge 33 commits into
luau-lang:masterfrom
karl-police:patch-4
Open

RFC: Name support for all Type Pack annotations#206
karl-police wants to merge 33 commits into
luau-lang:masterfrom
karl-police:patch-4

Conversation

@karl-police
Copy link
Copy Markdown
Contributor

@karl-police karl-police commented May 11, 2026

@DavidLoveLuau
Copy link
Copy Markdown

That's a good idea, but I think this syntax would more idiomatic for Luau:

type Foo<T...> = (T...) -> ()

type a = Foo<x: number, y: number, z: number>

@aatxe
Copy link
Copy Markdown
Member

aatxe commented May 11, 2026

I'm fairly certain the thing you actually want is for us to allow plain type packs (i.e. not in function parameters) to contain names, e.g. (a: number, b: string) being a valid type pack on its own. This would be much more practical and coherent than adding some new third syntax for writing types.

@Bottersnike
Copy link
Copy Markdown

How would this interact with, say

type A<T> = { foo: T }
A<[bar: number]>

? do we end up with a field named both foo but with a bonus bar name?

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

That's a good idea, but I think this syntax would more idiomatic for Luau:

type Foo<T...> = (T...) -> ()

type a = Foo<x: number, y: number, z: number>

If it would work, that would be cool too! Just a question about whether this would be allowed in the future

type a = x: number

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

I'm fairly certain the thing you actually want is for us to allow plain type packs (i.e. not in function parameters) to contain names, e.g. (a: number, b: string) being a valid type pack on its own. This would be much more practical and coherent than adding some new third syntax for writing types.

Are type packs, things that are within <> ?

e.g. <number>

if so, then yeah

I thought type packs are only when it is more than one type, e.g. T...

is (number) also considered a type pack?

would writing Type<(number)> conflict with () -> ()

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

How would this interact with, say

type A<T> = { foo: T }
A<[bar: number]>

? do we end up with a field named both foo but with a bonus bar name?

So, in this case A.foo would be [bar: number] though it could be discarded away if it wasn't put into a function annotation

@aatxe
Copy link
Copy Markdown
Member

aatxe commented May 11, 2026

Are type packs, things that are within <> ?

e.g.

if so, then yeah

I thought type packs are only when it is more than one type, e.g. T...

No, you can write both types and type packs within angle brackets because you can have both type parameters and type pack parameters. Type packs are just "any collection of zero or more types." The things on either side of the -> for a function type are both type packs, e.g. (x: number) -> string the x: number is a type pack with one type in it number and the associated name x, and the string is another type pack with one type in it. () is an empty type pack, and (x: number, y: string) -> string has x: number, y: string as a type pack for its arguments.

The syntax of Luau today doesn't currently allow you to write those type packs with names, which is what I would expect this RFC to actually be proposing, but you can write type packs in general, T... is a generic type pack that gets instantiated with a concrete type pack, for instance. It's the same relationship as T, a generic type, getting instantiated with a concrete type.

e.g.

type TakesAType<T> = { T }

type Instantiated = TakesAType<number>

type TakesATypePack<T...> = (T...) -> ()

type Instantiated2 = TakesATypePack<(number, string)>

Luau today does some context-aware determination for what is what, so TakesATypePack<number> still works and number is treated as a type pack of a single type, rather than a single type on its own. A likely more interpretable version of the distinction would probably require having to write (number,) to be a type pack, but in our syntax today, the parentheses aren't a required part of a type pack at all.

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

Are type packs, things that are within <> ?
e.g.
if so, then yeah
I thought type packs are only when it is more than one type, e.g. T...

No, you can write both types and type packs within angle brackets because you can have both type parameters and type pack parameters. Type packs are just "any collection of zero or more types." The things on either side of the -> for a function type are both type packs, e.g. (x: number) -> string the x: number is a type pack with one type in it number and the associated name x, and the string is another type pack with one type in it. () is an empty type pack, and (x: number, y: string) -> string has x: number, y: string as a type pack for its arguments.

The syntax of Luau today doesn't currently allow you to write those type packs with names, which is what I would expect this RFC to actually be proposing, but you can write type packs in general, T... is a generic type pack that gets instantiated with a concrete type pack, for instance. It's the same relationship as T, a generic type, getting instantiated with a concrete type.

e.g.

type TakesAType<T> = { T }

type Instantiated = TakesAType<number>

type TakesATypePack<T...> = (T...) -> ()

type Instantiated2 = TakesATypePack<(number, string)>

Luau today does some context-aware determination for what is what, so TakesATypePack<number> still works and number is treated as a type pack of a single type, rather than a single type on its own. A likely more interpretable version of the distinction would probably require having to write (number,) to be a type pack, but in our syntax today, the parentheses aren't a required part of a type pack at all.

Editing. I assume type packs count as one type?

Wondering what would happen in this case as well:

In the event where an annotation already has a name. The name would get overwritten if...

type Foo<T...> = (args: T...) -> ()
type Foo_2<T> = (arg: T) -> ()

type A = Foo<(number, number)> -- nothing changes about "args:"
type A = Foo<(x: number, y: number)> -- "args" would be gone if something substituted a generic
type C = Foo_2<(x: number)> -- same here for "arg"

@karl-police karl-police changed the title RFC: Syntax Type Labels RFC: Name support for all Type Packs annotations May 11, 2026
@karl-police karl-police changed the title RFC: Name support for all Type Packs annotations RFC: Name support for all Type Pack annotations May 11, 2026
@itsmath
Copy link
Copy Markdown

itsmath commented May 11, 2026

type Foo<T...> = (args: T...) -> ()
type Foo_2<T> = (arg: T) -> ()

type A = Foo<(number, number)> -- nothing changes about "args:"
type A = Foo<(x: number, y: number)> -- "args" would be gone if something substituted a generic
type C = Foo_2<(x: number)> -- same here for "arg"

The first example wouldn't work, since you can't currently give a name to the tail of your parameter list.
The second example also wouldn't work, because you wouldn't be able to give a name to a type, just to types in type packs. So you would need something like:

type Foo<T...> = (T...) -> ()

type C = Foo<(x: number)>

@itsmath
Copy link
Copy Markdown

itsmath commented May 11, 2026

The things on either side of the -> for a function type are both type packs

This makes me think that this syntax should be allowed: T... -> U..., currently you can only do (T...) -> U..., obviously you wouldn't be able to do something like this though: number, number -> U..., you'd need parenthesis, just like how the return type pack works today.

@karl-police
Copy link
Copy Markdown
Contributor Author

type Foo<T...> = (args: T...) -> ()
type Foo_2<T> = (arg: T) -> ()

type A = Foo<(number, number)> -- nothing changes about "args:"
type A = Foo<(x: number, y: number)> -- "args" would be gone if something substituted a generic
type C = Foo_2<(x: number)> -- same here for "arg"

The first example wouldn't work, since you can't currently give a name to the tail of your parameter list. The second example also wouldn't work, because you wouldn't be able to give a name to a type, just to types in type packs. So you would need something like:

type Foo<T...> = (T...) -> ()

type C = Foo<(x: number)>

Base is these syntax work

type E = (number)
type E2 = (number) -> ()

and RFC proposes this

type E = (x: number)
type E2 = (x: number) -> () -- already works no changes!

so all other (number, number) cases would let you put a name

@itsmath
Copy link
Copy Markdown

itsmath commented May 11, 2026

Base is these syntax work

type E = (number)
type E2 = (number) -> ()

and RFC proposes this

type E = (x: number)
type E2 = (x: number) -> () -- already works no changes!

so all other (number, number) cases would let you put a name

At least from what I understand:

type Fn<T> = (T) -> () -- T here isn't actually a type pack

type A = Fn<(number)> -- Even here, T still isn't a type pack, and something like Fn<(...number)> would be an error

So what you're actually proposing is that every type (not just type packs) can have an optional name, but I'm not sure if that is necessarily useful, and you'd need to decide what happens when you give a name to a type that already has a name:

type Fn<T> = (arg: T) -> ()

type B = Fn<(x: number)> -- Is this (arg: number) or (x: number)?

You don't have this ambiguity if you're only working with type packs, because type packs can't be assigned names, like I said earlier.

type Fn<T...> = (arg: T...) -> () -- This isn't something you can do at all

@aatxe
Copy link
Copy Markdown
Member

aatxe commented May 11, 2026

I assume type packs count as one type?

Type packs and types are just... different classes of thing. One type pack is one type pack. One type pack is never one type, but one type pack might consist of one type.

@aatxe
Copy link
Copy Markdown
Member

aatxe commented May 11, 2026

This makes me think that this syntax should be allowed: T... -> U..., currently you can only do (T...) -> U..., obviously you wouldn't be able to do something like this though: number, number -> U..., you'd need parenthesis, just like how the return type pack works today.

I think that would probably be the most consistent thing, for sure, but I'm not sure the grammar would hold up if we had actually allowed the parameter type pack to be unparenthesized.

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

so all other (number, number) cases would let you put a name

At least from what I understand:

type Fn<T> = (T) -> () -- T here isn't actually a type pack

type A = Fn<(number)> -- Even here, T still isn't a type pack, and something like Fn<(...number)> would be an error

This here does not error in https://play.luau.org/

--!strict
type E<T> = T
local e: E<(number)>

type E2<T> = (T) -> ()
local e2: E2<(number)>

@aatxe
Copy link
Copy Markdown
Member

aatxe commented May 11, 2026

Yes, that doesn't error, but no type packs are involved anywhere in that code. You have T, a generic type, and (number), a parenthesized concrete type, and you instantiate T with number.

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

You don't have this ambiguity if you're only working with type packs, because type packs can't be assigned names, like I said earlier.

type Fn<T...> = (arg: T...) -> () -- This isn't something you can do at all

Ah.

Well, I guess the most logical thing for this RFC then, is to leave, as arg: T even if substituted?

type Fn<T> = (arg: T) -> ()

As it is unclear for now, what should happen for this, and a different RFC could tackle it. 🤔

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

Yes, that doesn't error, but no type packs are involved anywhere in that code. You have T, a generic type, and (number), a parenthesized concrete type, and you instantiate T with number.

What if it was

type E<T> = (T)
local e: E<(x: number)>

would that work?

oh ok, I think I see it wouldn't work, because T... is a type pack, and T isn't.

And <number, number> is a type pack?

@itsmath
Copy link
Copy Markdown

itsmath commented May 11, 2026

And <number, number> is a type pack?

Not necessarily, it depends on what is being instantiated, for example:

type Example<T, U> = (T, U) -> ()

-- T becomes number and U becomes string, both are types
-- (number, string) -> ()
type A = Example<number, string>

type ExamplePack<T...> = (T...) -> ()

-- T... is a type pack
-- (number, string) -> ()
type B = ExamplePack<number, string>

Both A and B are the exact same type, but A is instantiated with two types whereas B is instantiated with a type pack

@karl-police
Copy link
Copy Markdown
Contributor Author

karl-police commented May 11, 2026

tried to update the RFC

this would be what would remain unsupported

We can't rename T here

type Foo<T> = (arg: T) -> ()
type A = Foo<(x: number)> -- (x: number), does not count as a type pack

-- Not possible!
type Bar<T,U> = (T,U) -> ()
local e: Bar<(x: number, y: number)>

@MagmaBurnsV
Copy link
Copy Markdown
Contributor

I think the design space here needs to be organized a bit more here, because this feels more like a lot of interesting ideas that just aren't unified yet.

First, there needs to be some definition for what a "type pack" actually is. Obviously, T... defines a formal generic type pack, and (T1, T2, ..., TN) defines an actual type pack, but things like (T1) aren't type packs unless they are a type argument for a generic type pack like so:

type Fn1<T> = (T) -> ()
type A = Fn1<(x: number)> -- not a type pack, so this is wrong

type Fn2<T...> = (T...) -> ()
type B = Fn2<(x: number)> -- but this is!

Second, the return thing of function types needs to be formalized as a type pack as well, so () -> (x: number) can also be valid. In type theory land, functions can just be defined as exponential types between two type packs, so this makes sense.

With these definitions, it becomes clear what is and isn't allowed:

  1. type A = (x: number) and type B = (x: number, y: number) aren't allowed, as type A = (number) isn't a type pack.
  2. type F<T> = (T) -> (); type A = F<(x: number)> also isn't allowed, since T isn't a formal type pack.
  3. type F<T...> = (arg: T...) -> () is impossible regardless, so substituting names isn't an issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants