Typechecking with OOP Metatables

I am trying to make a strict-typechecking compatible custom tween wrapper (OOP) class. I’ve seen this post regarding the same topic, but that solution did not work for me. My current code:

-- Setup --
local Tween = {}
Tween.__index = Tween

-- Lua Types --
type TweenInfoTable = {
	Time: number?,
	EasingStyle: Enum.EasingStyle?,
	EasingDirection: Enum.EasingDirection?,
	RepeatCount: number?,
	Reverses: boolean?,
	DelayTime: number?
}

type CustomTween = typeof(setmetatable({}, Tween)) & {
	Tween: Tween?,

	Object: Model | Instance,
	TweenInfo: TweenInfo,
	Data: { [string]: any },
	
	Play: () -> nil,
	Pause: () -> nil,
	Cancel: (Destroy: boolean) -> nil,
	Destroy: () -> nil,
}

-- Functions --
local TableToTweenInfo = function(Info: TweenInfoTable) : TweenInfo ... end

-- Constructor --
function Tween.new(Object: Model | Instance, Info: TweenInfoTable, Data: { [string]: any} ) : CustomTween
	local Info = TableToTweenInfo(Info)
	
	local _Tween = {
		Tween = nil,
		
		Object = Object,
		TweenInfo = Info,
		Data = Data,
	}
	
	setmetatable(_Tween, Tween)
	
	return _Tween
end

The error the linter is giving me is:

Type Error: (67,2) Type '{ @metatable Tween, { Data: {| [string]: any |}, Object: Instance | Model, ... 2 more ... } }' could not be converted into '{ @metatable Tween, { Data: {| [string]: any |}?, Object: (Instance | Model)?, ... 2 more ... } } & {| Cancel: (boolean) -> nil, Data: {| [string]: any |}, ... 6 more ... |}'
caused by:
  Not all intersection parts are compatible. Type '{ @metatable Tween, { Data: {| [string]: any |}, Object: Instance | Model, ... 2 more ... } }' could not be converted into '{| Cancel: (boolean) -> nil, Data: {| [string]: any |}, ... 6 more ... |}'
2 Likes

return setmetatable(_Tween :: any, Tween) :: CustomTween
not sure if this is the best way of going about it, but its how i do it and it seems to work

4 Likes

This is pretty old, but I rewrote your MT in a way that fixes most of your warnings without double casting.

type TweenInfoTable = {
	Time: number?,
	EasingStyle: Enum.EasingStyle?,
	EasingDirection: Enum.EasingDirection?,
	RepeatCount: number?,
	Reverses: boolean?,
	DelayTime: number?
}

type CustomTween = typeof(setmetatable(

	-- Property Table
	{}::{
		Tween     : (     Tween?     ),
		Object    : (Model | Instance),
		TweenInfo : (    TweenInfo   ),
		Data      : ( {[string]: any})
	},
	
	-- Metatable Portion
	{}::{
			
		----------- metamethods ----------- 
		__index: typeof(getmetatable( ({}::CustomTween) )), -- how you annotate a cyclic __index = MT reference
		
		------- inherited functions -------		
		new	   :   (
					-- args
					Object: Model | Instance,
					Info: TweenInfoTable,
					Data: { [string]: any}
				   ) 
					-- output 
			   ->  (CustomTween),

 
		Play   : (self: CustomTween) -> (),
		Pause  : (self: CustomTween) -> (),
		Destroy: (self: CustomTween) -> (),
		Cancel : (self: CustomTween, bDestroy: boolean?) -> (),
		
	}

))






-- Setup --
local Tween: CustomTween
Tween = setmetatable(

	-- Properties   (irrelevant)
	{
		Tween     = nil,
		Object    = Instance.new("Model"),
		TweenInfo = TweenInfo.new(),
		Data      = {}
	},

	-- MT Only       
	{	
		__index = getmetatable(Tween), -- this line silences typechecking

		-- Create a new Tween.		
		new = function (Object: (Model | Instance), Info: TweenInfoTable, Data: { [string]: any} ): (CustomTween)

			local convertedTWEEN = TweenInfo.new(
				Info.Time,
				Info.EasingStyle,
				Info.EasingDirection,
				Info.RepeatCount,
				Info.Reverses,
				Info.DelayTime
			)

			local newTween: CustomTween = setmetatable({
				Tween = nil,
				Object = Object,
				TweenInfo = convertedTWEEN,
				Data = Data,
			}, getmetatable(Tween)) -- I usually separate the MT and Properties so I'm not running getmetatable() a bunch but o well
			
			return newTween
		end,

		Play = function (self: CustomTween): ()

		end,

		Pause = function (self: CustomTween): ()

		end,
		
		Destroy = function (self: CustomTween): ()
	
		end,

		Cancel = function (self: CustomTween, bDestroy: boolean?): ()

			-- insert below here 

			-- insert above here 

			if bDestroy then
				self:Destroy()
			end
		end
	}
)

-- now add the __index            T1              KEY    =     VALUE
setmetatable(Tween, rawset(getmetatable(Tween), "__index", getmetatable(Tween)))

To my knowledge this is the only way to do it

3 Likes

That’s actually a really cool looking way! This is the way I have found works for me:


type BaseTable = {
   // ...properties and such
}

type Table = typeof(setmetatable({} :: BaseTable, Class); 

This will keep the inherited methods of the Class, and provide autocompletes automatically. I’ve run into a few problems where using multiple types can confuse the typechecker, or where accessing specific types within a method will cause an incompatibility error.