Short summary on strict metatable OOP

This guide is not for newcommers

Also, this guide will not cover any automatic typing patterns. This guide will only include old-school C-like strict typing; that way, you will properly understand how to get the hang of strictly type-checking the metatable OOP.

With that out of the way, let’s get to the main part


Why do we need strict metatable OOP?

Strict type-checking is required for any large-scale projects. Without it, you are left with insanity with whatever the classic metatable OOP is. Usually all of your variables are marked unknown, never, or even any. Only rarely do they get somewhat properly typed, but then once again it is quite unreliable.

So to avoid such “daemons,” we must type the variables. But it isn’t as easy as in… let’s say C#. In fact, it is a lot harder, not as easy as marking variables with type.

That’s where this guide will help you in strictly typing.


Code structure

We must understand the overall code structure, unlike in classic metatable OOP. In a strictly typed one, all of your methods are defined outside of the metatable. This is done for

  1. Retaining the comments
  2. Automatic argument typing of the function

You also need 3 types per object

  1. Property type
  2. Metatable type
  3. Object type

In the end you should have something like that:

--!strict

local M_Animal:AnimalMeta

--Creates a new animal
local function Animal_new(name:string) : Animal
   return setmetatable({Name = name},M_Animal)
end

export type AnimalMeta = {
   __index:AnimalMeta;
   new:typeof(Animal_new);
}

export type AnimalObj = {
   Name:string;
}

export type Animal = setmetatable<AnimalObj,AnimalMeta>

M_Animal = {
   new = Animal_new;
}::AnimalMeta
M_Animal.__index = M_Animal

This is quite unconventional in comparison to the classic metatable OOP. But it’s the only way I know of strictly typing it without involving automatic typing via typeof(obj).

Let’s now make some basic inheritance to show how to implement methods for the classes:

--!strict

local M_Animal:AnimalMeta

--Creates a new animal
local function Animal_new(name:string) : Animal
   return setmetatable({Name = name},M_Animal)
end

export type AnimalMeta = {
   __index:AnimalMeta;
   new:typeof(Animal_new);
}

export type AnimalObj = {
   Name:string;
}

export type Animal = setmetatable<AnimalObj,AnimalMeta>

M_Animal = {
   new = Animal_new;
}::AnimalMeta
M_Animal.__index = M_Animal

--Dog class

local M_dog:DogMeta

--Creates a dog class
local function Dog_new(color:string) : Dog
   return setmetatable({Name = "Good boy", Color = color},M_dog)
end

--Bark!
local function Dog_Bark(self:DogObj) : ()
   print(self.Name,"Barked!")
end

type DogMetaT = {
   __index = DogMeta;
   new:typeof(Dog_new);
   Bark:typeof(Dog_Bark);
}

export type DogMeta = setmetatable<DogMetaT,AnimalMeta>

export type DogObj = AnimalObj&{
   Color:string;
}

export type Dog = setmetatable<DogObj,DogMeta>

M_dog = {
   new = Dog_new;
   Bark = Dog_Bark;
}::DogMetaT

M_dog.__index = M_dog

You can now use it as you would via the constructor directly or the metatable, but with full autocompletion and without the type analysis yelling at you:

local Animal = Animal_new("Cow")
local Dog = M_dog.new("Brown")
Dog:Bark()

Verbose syntax

Unlike classic metatable OOP. In a strictly typed one, the syntax is extremely verbose, even somewhat comparable to the verbosity of C++ syntax. This makes it unfriendly for projects whose goal is compactness. But in large-scale projects it’s a must-have. Besides being able to type the object itself, you also get to type the metatable and the property table. Which allows you to have inheritance even on the typing level.

5 Likes