Type conversion issue

Hello all,

I am trying to use a static method in an OOP ModuleScript of class Object; I’m trying to mimic the use of a static method from JavaScript (I am porting JS to Luau):

Below is a skeleton of the class definition:

type ObjectImpl = {
    __index: ObjectImpl,
    new: (...) -> Object,
    method1: (value: Object, other: Object) -> Object,
    method2: (self: Object, ...) -> Object -- This technically doesn't need to retrun Object, since it mutates the original object? Unsure.
}

export type Object = typeof(setmetatable({} :: { ... }, {} :: ObjectImpl))

local Object: ObjectImpl = {} :: ObjectImpl
Object.__index = Object

-- Constructor
local function Object.new(...)
  local inst = {}
  ...
  setmetatable(inst, Object)
  return inst
end

-- Static method that returns an Object
local function Object.method1(value: Object, other: Object)
  ...
end

-- Object method that mutates object it is called on (modifies and returns self)
local function Object:method2(...)
  ...
end

return Object

I am trying to execute the following line, which is nested in another object method that mutates the Object it is called on.

local ObjectResult = Object.method1(Object:method2(...), Object.new(...))

…but whenever I use --!strict, I get the following error:

Type 'ObjectImpl' could not be converted into 'Object'

This issue only occurs with --!strict and works fine if I do not declare the script as strictly typed. It also works in strict mode if I replace Object:method2(...) with self:method2(...), but I do not want to mutate the object that method2 is working in. I have the above code typed in a way that follows the example from the official Luau typing guide.

Has anyone else experienced this issue, and if so, what is the solution if there is one? Additionally, is using self:method2() identical to what I am trying to achieve?

1 Like

Anyone have any ideas? I’m unsure as to how to change DecimalImpl to allow it to be converted to Decimal without an issue. Also, the beta type-checker throws many errors, so that is not a solution :frowning:

Object:method2 expects Object to be passed in as the first (self) argument. However, when you do Object:method2(...), you’re passing ObjectImpl as the first argument and that’s not correct. The type solver is saying there’s no implicit conversion from ObjectImpl to Object probably due to the latter having a metatable and the former not having one (not 100% sure on that though).

Based on your code, you can’t call methods on the implementation table directly using : because it’s expecting the metatable type to be passed in, not the implementation type.

1 Like

What’s stopping you from doing something like this?

local ObjectA = Object.new(...)
local ObjectResult = Object.method1(ObjectA:method2(...), Object.new(...))
1 Like

I suppose this is fine, but it’s not ideal - I would have rather not wanted to instantiate another object (although I’m sure the GC would collect it); it’s also kind of ugly :frowning: . I’m not sure how the this keyword from JS works under the hood - the typechecker is the only reason I can’t do this in the first place, since this works fine in a non-strict environment.

I just resolved on allowing the constructor to return a default Object when given no arguments and chaining together Object.new():method2(...).

I do wish the current iteration of the typechecker made it easier to work with metatables, but alas…

Does the JS library you’re porting do something like this without instantiating a separate object? I’m struggling to see how it would if method1 needs a reference to Object and not ObjectImpl.

The JS library I’m porting has a TypeScript file that defines all methods of the class as static. Most of these methods take two objects of type Class as arguments.

Here is the library. It’s a popular library for handling numbers of arbitrary magnitude up to 10^^308 (a power tower of 308 10s).

I will be honest and say it’s been a long while since I’ve wrote anything of scale in JavaScript, so I could be misunderstanding here. I ported this library to Lua a while back (say, 2-3 years ago) and I’ve been working on rewriting my port from scratch (including the LRU cache this time) as practice. Also, typedefs allow me to avoid a bunch of __index shenanigans I had to utilize in my first attempt at a port… so that’s a plus

So in you’re original example, you’re trying to port over something like this?

static min(value: DecimalSource, other: DecimalSource): Decimal;

Yes. In the OP, method1 is actually static mul(value: DecimalSource, other: DecimalSource): Decimal.

Oh okay. In that case, that library instantiates Decimal objects when it calls static Decimal functions like you’d do here in luau. You can see that starting from this line.

I don’t think there’s really a way around that.

1 Like

Gotcha - makes sense. Thanks! :slight_smile: