[Feature Request] Add a strong typed struct/class that cannot assign a new field that is not defined in the struct/class.
- Example:
---@struct Student
---@field study_level integer
---@type Student
local new_student = {
study_level = 0,
tobacco_level = 1, -- This should be error.
}
- Usage: We can do refactoring easier when rename the fields with this feature.
Similar previous discussions:
- https://github.com/LuaLS/lua-language-server/discussions/1602
- https://github.com/LuaLS/lua-language-server/discussions/1652
- https://github.com/LuaLS/lua-language-server/issues/1823
I'd like to see this too. I'm in favour of struct because it's similar to C#, but exact works fine too. I'm also liking other suggestions for built-in types like Exact<MyClass> instead of more @ styled notation because that might enable support for future features with a more modular approach.
For example, ReadOnly<T> could then also be added to change the behaviour of the wrapped generic type T where T is a collection data type (e.g., a table or list) to tell LuaLS that its read-only, or Callable<MyTable> might be good for declaring that a table has the __call meta-method so you can call it like a function. Who knows 🤷 but it sounds interesting.
Would you accept PRs regarding this or is this subject completely off the table?
Would you accept PRs regarding this or is this subject completely off the table?
I'm glad to accept PRs.
Really need this feature (for one, it catches typos). It shouldn't be too hard to implement?
Referencing an undefined variable emits an LSP error or warning (even regardless of the fact that it is not a runtime error to do so) and this is a good thing, so why wouldn't the same hold true for table indices?
IIRC, typescript disallows extra keys by default (or maybe it's opt-in behavior to have it that way, I don't remember), and then you can allow additional arbitrary keys on a type by it defining/declaring like this:
type MyObj = {
[key: string]: any;
definedKey1: boolean;
definedKey2: string;
...
instead of like this:
type MyObj = {
definedKey1: boolean;
definedKey2: string;
...
I don't think this is totally necessary however. A special symbol, type constructor, or additional annotation (kinda like @field is its own annotation and goes on its own line) would suffice (whether it's Freeze<> or Exact<> or ---@exact or whatever else).
An exact table for the input of functions is also something I would also be very interested in! Initially I was just using params everywhere for function input definitions, but wanted to switch over to using exact dicts as the input to functions utilizing @class, as I thought it made the code cleaner (especially in places where the arguments in should be 'nested').
One thing I would like to see though if this every makes it in, is the possibility to mark fields on classes as optional using the normal ? syntax. When using the @class I want to require some fields, allow but not require some other fields, and disallow everything else
My current idea is
---@class Student
---@exact
---@field study_level integer
or
---@class Student <exact>
---@field study_level integer
Any other good ideas are also welcome.
(Translated by ChatGPT)
I know it'll break existing code and require work to update the definitions everywhere, but would you consider simply changing the existing @class so that it enforces exactness by default? If a user wants to opt out for certain fields, they'll have to manually annotate them as having nil values:
---@field study_level? integer
-- or this, but I prefer the first syntax:
---@field study_level integer?
This is how TypeScript works and while Lua annotations and TypeScript are different, I think enforcing types to be exact by default is a more consistent design that forces the user to think about class fields and whether they should be required or optional.
Of course, I understand the amount of headache involved in updating large codebases and addon libraries, but I thought it's worth mentioning this option if you want to consider it for a future major version.
I agree with @firas-assaad . As a compromise this could be enabled by default when only strict is enabled
I know it'll break existing code and require work to update the definitions everywhere, but would you consider simply changing the existing so that it enforces exactness by default? If a user wants to opt out for certain fields, they'll have to manually annotate them as having nil values:
@class---@field study_level? integer -- or this, but I prefer the first syntax: ---@field study_level integer?This is how TypeScript works and while Lua annotations and TypeScript are different, I think enforcing types to be exact by default is a more consistent design that forces the user to think about class fields and whether they should be required or optional.
Of course, I understand the amount of headache involved in updating large codebases and addon libraries, but I thought it's worth mentioning this option if you want to consider it for a future major version.
No, because many users will declare fields using a mixed approach, for example:
---@class MyClass
---@field id integer
local M = Class 'MyClass'
M.showName = 'MyClass'
function M:init()
self.list = {}
self.id = nextID()
end
(Translated by ChatGPT)
No, because many users will declare fields using a mixed approach, for example:
---@class MyClass ---@field id integer local M = Class 'MyClass' M.showName = 'MyClass' function M:init() self.list = {} self.id = nextID() end(Translated by ChatGPT)
In that case the explicitly set fields are not optional unless they're nil-able, or do you meant that it's technically complex to implement?
Anyway it's just an idea if you want to consider a stricter and more consistent type system instead of adding more special keywords. One thing I notice with the language server is that because users (myself included) ask for a lot of orthogonal features, and there is usually one implementer, the annotations start to get more complex and more inconsistencies can arise.
To be clear, I think you're doing a great job managing all this complexity and carefully reviewing suggestions, but I bring up TypeScript because it's a mature language that went through some similar discussions, and the result is a more consistent and strict model that catches many user errors.
I know that I'm probably not thinking of a lot of the other use cases where my idea won't work. Having any kind of @exact annotation would still be extremely helpful. (I prefer ---@class Student <exact> in your example, assuming there will be new kinds of 'attributes' added to classes in the future)
I can try adding an option that is enabled by default to enable this feature. If many users give feedback that they don't like this feature, I can downgrade it to default disable and provide new annotations to manually mark it.
(Translated by ChatGPT)
I can try adding an option that is enabled by default to enable this feature. If many users give feedback that they don't like this feature, I can downgrade it to default disable and provide new annotations to manually mark it.
(Translated by ChatGPT)
That's a good compromise. I assume it'll still be in a new major version (4?) because I know this will break most of my existing code 😅
You can also make a separate pinned issue to announce this change and solicit more user feedback before doing it, like you did for some features in the past. That way more users will see it coming, at least.
It's also worth mentioning that TypeScript provides a Partial<className> syntax for cases where you want to opt-out of exact types and tell the compiler that all the fields are optional:
interface Person {
name:string;
address:string
age:number
}
let person:Partial<Person>= {} //No error. All Properties are now optional in person object
I can try adding an option that is enabled by default to enable this feature. If many users give feedback that they don't like this feature, I can downgrade it to default disable and provide new annotations to manually mark it.
(Translated by ChatGPT)
I like that idea. I think this feature is a big (good) one, even if it comes to be an opt-in feature.
@sumneko could it be released as a "preview" version on VS Code? This would allow users to opt in to this version, try it out, update their definitions, and provide feedback on it.
I do agree with the idea of strict by default, as TypeScript does it and it can help prevent a lot of errors – however, it can also add more work, which some may not want:
const myMap = new Map<string, number>();
// Property 'children' does not exist on type 'Map<string, number>'
myMap.children = [];
Need to add .children field somehow
interface ParentMap<K, V> extends Map<K, V> {
children: Map<K, V>[];
}
const myMap = new Map<string, number>() as ParentMap<string, number>;
myMap.children = [];
No, because many users will declare fields using a mixed approach, for example:
Thinking back about the example and what @carsakiller wrote, my suggestion to assume explicitly set fields (ones defined directly outside of the class annotation) are not optional would weaken the type system by not allowing the Language server to warn about unintentional typos. Preventing the definition of new fields was one of the reasons I started a discussion about such a feature https://github.com/LuaLS/lua-language-server/discussions/1602
I'm not sure what's the best approach in this case. Making classes strict by default will enable warnings if you forget to set a property, but allowing a mixed approach to define fields won't catch other kinds of errors. The mixed approach is particularly needed for defining class methods and self variables.
No, because many users will declare fields using a mixed approach, for example:
Thinking back about the example and what @carsakiller wrote, my suggestion to assume explicitly set fields (ones defined directly outside of the class annotation) are not optional would weaken the type system by not allowing the Language server to warn about unintentional typos. Preventing the definition of new fields was one of the reasons I started a discussion about such a feature #1602
I'm not sure what's the best approach in this case. Making classes strict by default will enable warnings if you forget to set a property, but allowing a mixed approach to define fields won't catch other kinds of errors. The mixed approach is particularly needed for defining class methods and
selfvariables.
Just to clarify, it would warn about typos in cases where you are using the class, just not in the case where you are defining the class (which for me is a nice trade-off, but I am sure there are others with differing opinions on this).
It might be able to warn about the use case in this issue (defining a new variable of a type) but it creates a new distinction between the following forms:
---@class Class
---@field y integer
local cls = { y = 1 }
cls.x = true -- no issues, x is now a required part of the `Class` class
---@type Class
local instance = { y = 1, x = false } -- warning if you forget y or x
instance.z = 'hi' -- warning?
That could be a valid compromise if that's technically possible, but it's a new concept for users to learn.
Another question is about self variables. Assuming we're using some OOP library like middleclass.
---@class Class
local Cls= class('Class')
function Cls:initialize(value)
self.x =value
end
function Cls:func()
self.y = 1
end
---@type Class
local instance = Cls() -- what does the language server think about `x` and `y`?
There are probably more examples I'm not thinking of. In this example it might be fine to assume that exact type checking is only done if you use the {} table initialization to set variables, so that it would still work with construction function calls.
A different idea is to treat all newly defined variables on the class as optional. That way all the self variables and the class functions are ignored when checking the required fields on an instance. If a user wants to explicitly make a self variable required they'll have to add an explicit @field annotation under the @class one.
With this approach and going back to my first example with cls.x = true on the class, x type should be viewed a non-required boolean. It's not assumed to be nil within the class definition (exactly as it works right now), but users defining new instances of the class don't have to explicitly specify it either.
That means you probably want a new distinction in the language server between being required and being nil-able. One idea is to differentiate the following two forms: ---@field required_var integer? and ---@field optional_var? integer?. Also see this topic about required nullable arguments https://github.com/LuaLS/lua-language-server/issues/2070#issuecomment-1510456403
Finally, if we do end up making fields required by default, then in addition to adding a configuration to disable this behavior, I'd also suggest adding a mechanism for users to tell the language server that they know what they're doing and want to explicitly add new properties to a type. (same way we do with @cast) That would provide a workaround for @carsakiller's example. Alternatively, users can disable that specific diagnostic for a single line.
I can do this first:
---@class Class
local m = {}
m.xx = 1 -- OK
---@type Class
local m
m.xx = 1 -- OK
m.yy = 1 -- Warning
I'm not checking the assignments inside class declarations temporarily because I still haven't thought of a good way to differentiate this case:
---@class Class
local m = {}
function m:init()
m.xx = true -- should OK
end
function m:doSomething()
m.xxx = false -- missspell, should warning
end
How about something like a @sealed annotation? Then this can be opt-in for specific classes.
---@class Class
local m = {}
m.xx = 1 -- OK
---@type Class
local m
m.xx = 1 -- OK
m.yy = 1 -- OK
---@class Class
---@sealed
---@field xx integer
local m = {}
m.xx = 1 -- OK
---@type Class
local m
m.xx = 1 -- OK
m.yy = 1 -- Warning
@sealed meaning the class cannot have new members dynamically added to it.
The downside of this would mean all methods would need to be declared up front.
The downside of this would mean all methods would need to be declared up front.
I cannot allow injecting fields from type references; if I did, the cost of correctly collecting these fields would be unacceptable. Therefore, you must use ---@class to inject fields.
Ah sorry, I should have changed the example so m isn't aliased.
What I meant was this.
---@class Class
local m = {}
m.xx = 1 -- OK
m.yy = 1 -- OK
---@class Class
---@sealed
---@field xx integer
local m = {}
m.xx = 1 -- OK
m.yy = 1 -- Warning
However, I think I now understand your suggestion better. Are you saying any fields defined on the initial @class variable become fields:
---@class Class
local m = {} -- Anything assigned to `m` is added as a field of `Class`
m.xx = 1 -- OK: adds `xx` as a field to `Class`
---@type Class
local m2 -- `m2` is an instance of `Class` with the fields: `xx`.
m2.xx = 1 -- OK
m2.yy = 1 -- Warning
I think this is a good idea since it doesn't require any additional annotation syntax.
For the other case how about:
---@class Class
local m = {}
function m:init()
m.xx = true -- OK: adding field `xx` to `m`
self.xx = true -- OK: `xx` is a field of `m`
self.yy = true -- OK: `yy` is a field of `m`
m.yy = true -- OK: adding field `yy` to `m`
end
function m:doSomething()
self.xxx = false -- WARNING: m does not have field `xxx`
end
Basically we need to check all fields that can ever be assigned m even if done dynamically.
For the other case how about:
---@class Class local m = {} function m:init() m.xx = true -- OK: adding field `xx` to `m` self.xx = true -- OK: `xx` is a field of `m` self.yy = true -- OK: `yy` is a field of `m` m.yy = true -- OK: adding field `yy` to `m` end function m:doSomething() self.xxx = false -- WARNING: m does not have field `xxx` endBasically we need to check all fields that can ever be assigned
meven if done dynamically.
This does not conform to the writing habits of most people
@carsakiller any suggests for name and syntax of exact or sealed ?
Can I ask an example(s) of typical writing habits you wish to support?
My suggestion works up your suggestion by defining a base table that acts as a definition of the class.
Can I ask an example(s) of typical writing habits you wish to support?
My suggestion works up your suggestion by defining a base table that acts as a definition of the class.
---@class Class
---@field a number -- priority 1
local m = {
b = 2, -- priority 2
}
m.c = 3 -- priority 3
function m:init()
self.d = 4 -- priority 4
end
@carsakiller any suggests for name and syntax of
exactorsealed?
I think exact is better because sealed in C# means you cannot inherit from it. I'd prefer either exact or strict personally.
Would it be unreasonable for d to be required to be a field with @field for @exact classes?
If not, the benefits of this feature are reduced since it won't catch bugs of incorrectly assigned fields in methods.
Otherwise, @firas-assaad suggestion of making d become an optional field seems ok.
I think exact is better because sealed in C# means you cannot inherit from it. I'd prefer either exact or strict personally.
Good point. This is the same in scala.