From de65e7b60023c67e6e50ed0cfe6c84b33f58f6b9 Mon Sep 17 00:00:00 2001 From: VegetationBush <98243286+VegetationBush@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:22:18 -0500 Subject: [PATCH 1/6] Implement type-only imports in Luau Add type-only imports to Luau, allowing type importing without runtime dependencies. --- docs/type-only-importing.md | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/type-only-importing.md diff --git a/docs/type-only-importing.md b/docs/type-only-importing.md new file mode 100644 index 00000000..e5456120 --- /dev/null +++ b/docs/type-only-importing.md @@ -0,0 +1,91 @@ +# Type-only imports + +## Summary + +Add type-only imports to Luau, allowing code to import types without creating a runtime dependency. This enables support for importing interfaces, type aliases, and dependency injection. + +## Motivation + +Assume the creation of a `Computer` and `Mouse` class. Computer uses `Mouse` as a component, and `Keyboard` requires `Computer` as an argument. Typically, the first instinct is to create something like: +```lua +-->> computer +local Mouse = require(script.Mouse) +local function createComputer() + local self = setmetatable({}, Computer):: Computer + self.Mouse = Mouse.new(self) + return self +end +``` +```lua +-->> mouse +local Computer = require(script.Parent) +local function createMouse(Computer: Computer.Computer) + return setmetatable({Computer = Computer}, Mouse):: Mouse +end +``` +However, it becomes clear that this is impossible, since this causes a cyclic dependency between `Computer` and `Mouse`. The logical next step would be to create a third container storing the `Computer` type, of which both `Computer` and `Mouse` require from it: +```lua +-->> computer +local Mouse = require(script.Mouse) +local __types = require(script.__types) +local function createComputer() + local self = setmetatable({}, Computer):: __types.Computer + self.Mouse = Mouse.new(self) + return self +end +``` +```lua +-->> mouse +local __types = require(script.__types) +local function createMouse(Computer: __types.Computer) + return setmetatable({Computer = Computer}, Mouse):: __types.Mouse +end +``` +This works, but can become very cumbersome very quickly, especially if `Computer` also depends on other user-generated types, which may or may not generate their own cyclic dependencies. +Reason being, the current type importing state of Luau is a clear limiting factor when considering pratices like dependency injection. + +The above problem can easily be solved if code can directly import types, removing the need to depend on the module containing the types. +```lua +-->> computer +local Mouse = require(script.Mouse) +local function createComputer() + local self = setmetatable({}, Computer):: Computer + self.Mouse = Mouse.new(self) + return self +end +``` +```lua +-->> mouse +type Computer = typeget(script.Parent) --> no cylic dependency +local function createMouse(Computer: Computer) --> intellisense on "Computer" remains + return setmetatable({Computer = Computer}, Mouse):: Mouse +end +``` + +## Design + +Preferably, the desgin should adhere to the general appearance of Luau. +Here are two proposed syntaxes: +```lua +type Apple, Banana = typeget(script.Fruits) +type { Apple, Banana } = typeget(script.Fruits) +``` +`type Apple, Banana = typeget(script.Fruits)` is more vanilla to Luau, but `type { Apple, Banana } = typeget(script.Fruits)` makes more semantic sense since type importing is unordered and named. + +Optionally, inline type extraction: +```lua +type User = require("./UserRepository").User +``` + +This feature incidentlly also alleviates Luau's wierd `ModuleName.ModuleType` syntax. + +Semantics: +- Type-only imports are erased at runtime (or used for optimization). +- No Lua code is emitted, so no runtime require happens. +- Regular type imports directly from other code continue to behave as usual. + +## Drawbacks +More keywords. Possible confusion with type extraction via `require`. `require` essentially becomes a `typeget` and a code emitter combined. + +## Alternatives +As aforementioned, an alternative could be a third, intermediary module. But as explained, could quickly become cumbersome. From c3ae1c63b87eac3b570b54a07e0049bb4d1536ee Mon Sep 17 00:00:00 2001 From: VegetationBush <98243286+VegetationBush@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:26:45 -0500 Subject: [PATCH 2/6] Clarity changes --- docs/type-only-importing.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/type-only-importing.md b/docs/type-only-importing.md index e5456120..ccdf76d1 100644 --- a/docs/type-only-importing.md +++ b/docs/type-only-importing.md @@ -2,11 +2,11 @@ ## Summary -Add type-only imports to Luau, allowing code to import types without creating a runtime dependency. This enables support for importing interfaces, type aliases, and dependency injection. +Add type-only importing to Luau, allowing code to import types without creating a runtime dependency. ## Motivation -Assume the creation of a `Computer` and `Mouse` class. Computer uses `Mouse` as a component, and `Keyboard` requires `Computer` as an argument. Typically, the first instinct is to create something like: +Assume the creation of a `Computer` and `Mouse` class. Computer uses `Mouse` as a component, and `Keyboard` requires `Computer` as an argument. Typically, the first instinct is to use dependency injection: ```lua -->> computer local Mouse = require(script.Mouse) @@ -23,7 +23,7 @@ local function createMouse(Computer: Computer.Computer) return setmetatable({Computer = Computer}, Mouse):: Mouse end ``` -However, it becomes clear that this is impossible, since this causes a cyclic dependency between `Computer` and `Mouse`. The logical next step would be to create a third container storing the `Computer` type, of which both `Computer` and `Mouse` require from it: +It becomes clear that retaining intellisense in this situation is impossible, since there is a cyclic dependency between `Computer` and `Mouse`. The logical next step would be to create a third container storing the `Computer` type, of which both `Computer` and `Mouse` require from it: ```lua -->> computer local Mouse = require(script.Mouse) @@ -41,8 +41,7 @@ local function createMouse(Computer: __types.Computer) return setmetatable({Computer = Computer}, Mouse):: __types.Mouse end ``` -This works, but can become very cumbersome very quickly, especially if `Computer` also depends on other user-generated types, which may or may not generate their own cyclic dependencies. -Reason being, the current type importing state of Luau is a clear limiting factor when considering pratices like dependency injection. +This works, and intellisense is restored. Unfortunately, this can become very cumbersome very quickly, especially if `Computer` also depends on other user-generated types, which may or may not generate their own cyclic dependencies. Basically, the current type importing state of Luau is a clear limiting factor when considering pratices like dependency injection. The above problem can easily be solved if code can directly import types, removing the need to depend on the module containing the types. ```lua From 484ccf83adad740b9d1056083861ae0e98465c58 Mon Sep 17 00:00:00 2001 From: VegetationBush <98243286+VegetationBush@users.noreply.github.com> Date: Sun, 15 Feb 2026 00:09:51 -0500 Subject: [PATCH 3/6] spelling mistake --- docs/type-only-importing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/type-only-importing.md b/docs/type-only-importing.md index ccdf76d1..afc72aa8 100644 --- a/docs/type-only-importing.md +++ b/docs/type-only-importing.md @@ -63,7 +63,7 @@ end ## Design -Preferably, the desgin should adhere to the general appearance of Luau. +Preferably, the design should adhere to the general appearance of Luau. Here are two proposed syntaxes: ```lua type Apple, Banana = typeget(script.Fruits) From c3a8f111b3c0ba19d66b80e847a2319f611ba5cd Mon Sep 17 00:00:00 2001 From: VegetationBush <98243286+VegetationBush@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:08:56 -0500 Subject: [PATCH 4/6] Reword --- docs/type-only-importing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/type-only-importing.md b/docs/type-only-importing.md index afc72aa8..ec793e5e 100644 --- a/docs/type-only-importing.md +++ b/docs/type-only-importing.md @@ -23,7 +23,7 @@ local function createMouse(Computer: Computer.Computer) return setmetatable({Computer = Computer}, Mouse):: Mouse end ``` -It becomes clear that retaining intellisense in this situation is impossible, since there is a cyclic dependency between `Computer` and `Mouse`. The logical next step would be to create a third container storing the `Computer` type, of which both `Computer` and `Mouse` require from it: +Here, a cyclic dependency between `Computer` and `Mouse` makes retaining intellisense impossible. A popular workaround is introducing a third module solely for type definitions; essentially, a header file: ```lua -->> computer local Mouse = require(script.Mouse) @@ -41,9 +41,9 @@ local function createMouse(Computer: __types.Computer) return setmetatable({Computer = Computer}, Mouse):: __types.Mouse end ``` -This works, and intellisense is restored. Unfortunately, this can become very cumbersome very quickly, especially if `Computer` also depends on other user-generated types, which may or may not generate their own cyclic dependencies. Basically, the current type importing state of Luau is a clear limiting factor when considering pratices like dependency injection. +While this restores intellisense, it becomes cumbersome as more types and dependencies are added. The header file method works very well for types that can be created without the importing of external types, or put simply, a user-defined type that doesn't depend on other user-defined types. -The above problem can easily be solved if code can directly import types, removing the need to depend on the module containing the types. +An arguably more elegant solution is to allow code to directly import types, avoiding runtime dependencies: ```lua -->> computer local Mouse = require(script.Mouse) From dfbbec3cbf7ee09ba51251e7182bda44578505fc Mon Sep 17 00:00:00 2001 From: VegetationBush <98243286+VegetationBush@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:21:38 -0500 Subject: [PATCH 5/6] More ideas --- docs/type-only-importing.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/type-only-importing.md b/docs/type-only-importing.md index ec793e5e..c5f0f95b 100644 --- a/docs/type-only-importing.md +++ b/docs/type-only-importing.md @@ -73,18 +73,37 @@ type { Apple, Banana } = typeget(script.Fruits) Optionally, inline type extraction: ```lua -type User = require("./UserRepository").User +type User = typeget("./UserRepository").User ``` -This feature incidentlly also alleviates Luau's wierd `ModuleName.ModuleType` syntax. +Alternatively, extend the behavior of `require` to act differently when expecting a type vs. dependency. Using the examples above: +```lua +-->> this +type Apple, Banana = require(script.Fruits) + +-->> or this +type { Apple, Banana } = require(script.Fruits) + +-->> and/or this +type Apple = require(script.Fruits).Apple +``` + +This feature incidentlly alleviates Luau's wierd `ModuleName.ModuleType` syntax. Semantics: - Type-only imports are erased at runtime (or used for optimization). -- No Lua code is emitted, so no runtime require happens. +- No Luau code is emitted, so no runtime require happens. - Regular type imports directly from other code continue to behave as usual. ## Drawbacks -More keywords. Possible confusion with type extraction via `require`. `require` essentially becomes a `typeget` and a code emitter combined. +For the case of `typeget`: +- More keywords. +- Possible confusion with type extraction via `require`. `require` essentially becomes a `typeget` and a code emitter combined. + +For the case of extending the functionality of `require`: +- Slightly more ambiguous functionality. +- Code importing `require` and type importing `require` may not be immediately differentiable at a glance. +- No longer a function. ## Alternatives -As aforementioned, an alternative could be a third, intermediary module. But as explained, could quickly become cumbersome. +Use a header file for type definitions. Although, this scales poorly and can become cumbersome in larger projects. From 35fe25ec2ded5a3b356ebaff8d4112be8742429b Mon Sep 17 00:00:00 2001 From: VegetationBush <98243286+VegetationBush@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:41:15 -0500 Subject: [PATCH 6/6] Update type-only-importing.md --- docs/type-only-importing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/type-only-importing.md b/docs/type-only-importing.md index c5f0f95b..515d04bc 100644 --- a/docs/type-only-importing.md +++ b/docs/type-only-importing.md @@ -106,4 +106,4 @@ For the case of extending the functionality of `require`: - No longer a function. ## Alternatives -Use a header file for type definitions. Although, this scales poorly and can become cumbersome in larger projects. +Use a header file for type definitions. Although, this scales poorly and can become cumbersome in larger projects. The current workarounds do not meaningfully solve the cyclic dependency problem, either.