Skip to content

Best way to implement something like an abstract base class? #2453

Open

Description

@emmericp

I'm working on a framework where users of it implement a kind of module that mixes their provided logic with some optional event handlers.

Here is a simple example

-- provided by framework
---@class Base
---@field OnClick fun(self: Base, x: number, y: number)
local base = {}

---@generic T: Base
---@param name `T`
---@return T
function New(name)
	return setmetatable({}, {__index = base})
end

-- usage of framework by user

---@class MyImpl: Base
local foo = New("MyImpl")

function foo:OnClick(x, y) -- x and y are inferred as any instead of number :(
end

function foo:myHelperFunction()
end

So I expect the user to implement OnClick and fill it with their logic. My goal here is that the user automatically gets the correct types on the parameters for foo:OnClick. Here in this simple example it's just two numbers, but my real app has like > 10 such methods and some of these take complex types as parameters.

With the code above it doesn't know anything about the parameters in OnClick and shows them as any. Maybe I got something wrong with the generics, but even without all the fancy generic stuff (just @return base on New) it's the same problem.

The only work-around I found was to not use @class on the user-defined class and not define a generic, this makes the parameters on the OnCLick function definition correct! However, it now triggers injected-field on all user-defined values not defined in the base class like the myHelperFunction method :(

Activity

  1. tmillr commented on Jan 29, 2024

    @tmillr
    Contributor

    No you didn't get anything wrong afaik. I wish this was supported too.

    However, it now triggers injected-field on all user-defined values not defined in the base class like the myHelperFunction method :(

    You (or the downstream user) could always just manually ignore those errors I suppose (using ---@diagnostic), and I think those funcs/methods would still get applied to the derived class type.

    Also, FWIW, there's this lol:

    ---@class MyImpl: Base
    local foo = New("MyImpl")
    
    ;(--[[@as Base]]foo).OnClick = function(self, x, y)
    
    end
    
    function foo:myHelperFunction()
    end

    ...but that would have to be done manually for each overridden method, and this LSP doesn't necessarily warn you or make it obvious when you are doing so (overriding).

    Another hack that might work (in some sense?):

    local foo = New {
        OnClick = function(self, x, y)
    
        end,
    
        myHelperFunction = function()
    
        end
    }

    but then users would be required to call New() like this every single time...

    Hopefully this gets fixed or improved if/when the LSP is refactored (#2366).

  2. emmericp commented on Jan 30, 2024

    @emmericp
    ContributorAuthor

    FWIW I implemented this as a plugin for our stuff that just injects a @Class annotation together with a lot of @field annotations: https://github.com/DeadlyBossMods/LuaLS-Config/blob/main/DBM-Plugin.lua

  3. bavalpey commented on Feb 12, 2024

    @bavalpey
    Contributor

    I'm looking through your solution, @emmericp and I can't wrap my head around what exactly you've done to accomplish this.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions