Skip to content

Feature Request: Typestate / Self-Type Refinement for Method Calls #3407

@Gustavo10Destroyer

Description

@Gustavo10Destroyer

Problem

Lua is a highly dynamic language, and many real-world Lua frameworks mutate object capabilities at runtime.

This pattern is especially common in:

  • game engines
  • UI frameworks
  • ECS architectures
  • builder APIs
  • runtime mixin systems
  • userdata bindings from native engines

Currently, LuaLS/LuaCats cannot properly express:

"after calling this method, self now has additional capabilities/methods"

This becomes a major DX limitation for frameworks that dynamically extend objects.


Real-world Example

Consider a UI framework where all elements start as a generic UIElement.

---@class UIElement
local UIElement = {}

Calling:

element:setupUIImage()

injects image-related functionality into the element at runtime:

element:setImage(...)

However, LuaLS still sees element as only UIElement.

The only current workaround is manual casting:

element:setupUIImage()

---@cast element UIImage
element:setImage(material)

This works technically, but becomes repetitive and hurts DX significantly in large codebases.


Comparison to TypeScript

TypeScript supports type refinement after method calls through assertion signatures such as:

asserts this is SomeType

This allows APIs to safely refine object types after capability-changing methods.

A good real-world example is discord.js, where Interaction objects can be refined into subtypes through methods like:

interaction.isButton()
interaction.isChatInputCommand()

After refinement, TypeScript understands the new subtype automatically.

Lua frameworks often use similar runtime patterns, but LuaLS currently cannot model them.


Proposed Solution

Introduce a LuaCats annotation for self-type refinement / capability injection.

Possible syntax examples:

---@injects UIImage
function UIElement:setupUIImage() end

or:

---@mutates self UIImage
function UIElement:setupUIImage() end

or:

---@becomes UIImage
function UIElement:setupUIImage() end

Expected Behavior

After:

element:setupUIImage()

LuaLS would understand:

element :: UIElement & UIImage

allowing:

element:setImage(...)

without requiring manual casts.


Why This Matters

This would greatly improve support for:

  • dynamic Lua architectures
  • runtime capability injection
  • engine bindings
  • UI frameworks
  • builder APIs
  • fluent APIs
  • mixin systems

while keeping Lua's dynamic nature intact.

This feature would also reduce:

  • repetitive ---@cast
  • noisy annotations
  • inaccurate supersets in base classes
  • degraded autocomplete quality

Additional Notes

This feature does not need to become full typestate analysis or advanced dependent typing.

Even a pragmatic flow-sensitive refinement system limited to:

  • direct method calls
  • current scope
  • explicit annotations only

would already provide massive DX improvements.


Example Use Case

---@class UIImage
---@field setImage fun(self: UIImage, material: string)

---@class UIElement

---@injects UIImage
function UIElement:setupUIImage() end

local element = UIElement.new()

element:setupUIImage()

element:setImage("icon")

Expected:

  • autocomplete works
  • diagnostics recognize setImage
  • no manual cast required

Thanks for all the amazing work on LuaLS and LuaCats.
This would significantly improve tooling support for dynamic Lua patterns commonly used in real-world frameworks and game engines.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions