diff --git a/docs/type-pack-names.md b/docs/type-pack-names.md new file mode 100644 index 00000000..9175964d --- /dev/null +++ b/docs/type-pack-names.md @@ -0,0 +1,186 @@ +# Names in Type Packs + +## Summary + +Allowing users to always provide names within type packs ``(T...)``, so that parameter names can be attached to types. +This improves IntelliSense and autocomplete, and allows custom callbacks to show you argument names instead of just the type. + +## Motivation + +Currently, you can only provide argument names in a type by doing the following. + +```luau +type Callback = (x: number, y: number, z: number) -> () + +local cb: Callback = function() + --[[ + IntelliSense shows you + cb: (x: number, y: number, z: number) -> (). + Luau Playground just shows + cb: (number, number, number) -> () + --]] +end +``` + +But in the IntelliSense case, a developer would immediately know what each of these type _represents_. In this case ``x``, ``y`` and ``z``. + +Luau and any embedder internally within the implementation are able to define argument names in some way or another to types _procedurally_. +But a developer is not able to replicate everything from the implementation itself. + +
+ +Generic packs exist + +```luau +type WrappedSignal = { + FireToFoo: (self: WrappedSignal, targetFoo: Foo, T...) -> (), +} + +type MySignal = WrappedSignal +``` + +But in this case, the result of ``MySignal.FireToFoo`` would show up as: +```luau +(WrappedSignal, Foo, number, string) +-- or +(self: WrappedSignal, targetFoo: Foo, number, string) +``` +There is no context or indication on what these provided types ``(number, string)`` are meant to represent. +You don't want to use a function annotation to fill in ``T...`` because we want ``(self: WrappedSignal, targetFoo: Foo, number, string)`` +and not ``(self: WrappedSignal, targetFoo: Foo, (number, string) -> ())``. + +But you can see that ``targetFoo: Foo`` is present, but only because function annotations specifically let you describe argument names, which APIs can pick-up on. + +
+ +Luau already allows you to provide parameter names within function annotations. +```luau +(x: number) -> () +``` + +But when generic type packs are used, this information is no longer available. +```luau +type Callback = (T...) -> () +``` + +``T...`` will only emit over types but not any names/labels. + +And currently, there isn't a way to provide a name/label when using ``Callback`` + + +## Design + + +The idea is to allow to describe names in any valid type pack context. + +Allowing you to do ``Type<(x: number)>`` if the base is ``Type`` without being exclusively restricted to function annotations anymore. + +Names/Labels are only metadata: +- Names/Labels are NOT enforced in any type checking way +- They don't affect the type itself +- They only improve front-end hints +- Nothing changes about the types own identity, it just gets associated with a name. +- The name is NOT forced and are completely optional. +- Names propagate through type aliases. + +### Usage and Examples: + +```luau +type Callback = (T...) -> () + +function Foo(cb: Callback<(targetBar: Bar)> end +-- cb shows (targetBar: Bar) -> () +``` + +```luau +type Bar = (T) -> () +type A = Bar<(num: number)> -- not valid, '(T)' is not a type pack! +type MyNumber = (num: number) -- not valid not a type pack! +``` + + +```luau +type Foo = (T...) -> () + +-- Names are optional +type A = Foo<(a: number, number, label: number)> -- allowed + +type B = Foo<(x: number), (y: number)> -- not valid, they're not type packs +type C = Foo<(x: number, y: number)> -- valid + +type D = (num: number) -> () -- already works in Luau, no changes! +type E = () -> (num: number) -- valid +``` + +```luau +type Mixed = (A, T...) -> () +type B = Mixed -- valid, T... is a type pack, A is not +type B = Mixed<(text: string), (x: number, y: number)> -- not valid "A", is not a type pack +``` + +
+ +```luau +type WrappedSignal = { + FireToFoo: (self: WrappedSignal, targetFoo: Foo, T...) -> (), +} + +type MySignal = WrappedSignal<(amount: number, message: string)> +--[[ + MySignal.FireToFoo would become + (self: WrappedSignal, targetFoo: Foo, amount: number, message: string) -> () +]] +``` + + + +
+ + +```luau +type Func = () -> (x: number, y: number, z: number) +function Func2(): (a: number, b: number, c: number) + return 1,2,3 +end +``` + + + + + +## Drawbacks + +- Most likely none, other than the implementation. +- Union or intersections may keep a random name. + + +This RFC doesn't solve that we can't rename ``T`` here. +```luau +type Foo = (arg: T) -> () +type A = Foo<(x: number)> -- (x: number), does not count as a type pack + +-- Not possible! +type Bar = (T,U) -> () +local e: Bar<(x: number, y: number)> +``` + + +## Alternatives + +- A new syntax where it would be ``[x: number, y: number, z: number]`` or ``[x: number], [y: number], [z: number]``, that can be applied into types directly, other than within the type pack. +
+ +- Type functions could expose modifying argument names, but that would only work for function annotations, not on a type alone. +There wouldn't be a way to attach a label to a type. + +```luau +type WrappedSignal = { FireToFoo: (self: WrappedSignal, targetFoo: Foo, T...) -> (); } +type MySignal = WrappedSignal<(label1: number, label2: string) -> ()> +``` +And this would also mean that ``WrappedSignal`` needs to implement a type function now, that takes out the argument names and transforms ``.FireToFoo``. +And that sounds too complicated. + + +You don't want it to be ``( targetFoo: Foo: Player, (label1: number, label2: string) -> () ) -> ()`` + +You want ``(targetFoo: Foo: Player, label1: number, label2: string) -> ()``.