How do I typecheck a subclass with the new type solver?

Basically a follow up on this.

I am trying to implement type checking for GuidedMissile which is a subclass of Missile. I am using the new luau type solver, which is in beta.

Missile Class
local Missile = {}
Missile.__index = Missile

local function new(speed: number?, name: string?)
	local self = {}
	self.speed = speed or 100
	self.name = name or "default"
	--type Self = typeof(Missile) & typeof(self)
	return setmetatable(self, Missile)
end

export type Missile = typeof(new())

function Missile.move(self: Missile, dt: number)
	print("moved "..tostring(self.speed * dt))
end

function Missile.warn(self: Missile) 
	print("Missile "..self.name.." is about to explode!")
end

return {
	new = new,
	class = Missile
}
Guided Missile Class (Incomplete)
--!strict
local Missile = require(script.Parent.Missile)

local GuidedMissile = setmetatable({}, Missile.class)
GuidedMissile.__index = GuidedMissile

local function new(target: Part?, speed: number?, name: string?)
	local self = Missile.new(speed, name)
	self.target = target or workspace.dummy
	return setmetatable(self, GuidedMissile)
end

export type GuidedMissile = typeof(new(workspace.dummy))

function GuidedMissile.Fire(self: GuidedMissile, countdown: number?) 
	local str = self.name.." is firing at "..self.
	print(str)
end

Currently the problem is in the new() function, when I try to add new fields to a Missile:

Another type error:

Regardless of whether I keep the --!strict at the top of the script, the autocomplete is quite broken. As you can see, there’s no target field:

I tried to fix it by doing this instead:

I don’t like this way, because I have to add fields which were already defined in the super class’s constructor, leading to repetition. But it does seem to bring back the target field:

The unfortunate side effect, as you can see: no methods in autocomplete. (With or without the type annotation)

How can I add typechecking to this class and get functional intellisense? While also keeping the code elegant and free of repetition?

The problem is the table type is ‘sealed’ once you return it, so later additions to self in your GuidedMissle constructor aren’t allowed.

local function CreateRecord()
	return {Name = "John Smith"}
end

local MyRecord = CreateRecord()
MyRecord.Age = 5 -- no good, the type is sealed

Because of this, you’ll have to manually type every field and combine them using an intersection. Then you can force-cast the constructor into your new frankenstein type.

export type Missile = setmetatable<
	{
		speed: number,
		name: string
	},
	typeof(Missile)
>

export type GuidedMissile = setmetatable<
	Missile & {
		target: Part
	},
	typeof(GuidedMissile)
>

local function new(target: Part?, speed: number?, name: string?)
	local self = (setmetatable(Missile.new(speed, name), GuidedMissile) :: any) :: GuidedMissile
	self.target = target or workspace.Baseplate
	
	return self
end

Obviously, this is extremely verbose and still wouldn’t work for most scenarios (I actually had to switch off incremental typechecking to prevent a cyclic type error here).

My advice would be to ditch modelling data in an object-oriented fashion. I don’t see why you can’t just use normal tables and functions for this scenario.

type Missile = {
	speed: number,
	name: string
}

type GuidedMissile = Missile & {
	target: Part
}

local function moveMissile(self: Missile, dt: number) end
local function fireGuidedMissile(self: GuidedMissile) end

local myMissile = {speed = 100, name = "bobby"}
local myGuidedMissile = {speed = 150, name = "jimmy", target = workspace.dummy}

moveMissile(myMissile, 0.1)
moveMissile(myGuidedMissile, 0.1) -- look ma! polymorphism!

fireGuidedMissile(myGuidedMissile)
2 Likes

Luau is not very good, sadly. I’m sad every time I try to write typed code. It’s like a watered down TypeScript for Lua, I don’t know why they didn’t add everything TypeScript has. It’s not like they don’t have the resources…