Step 7 of 14, updated

Interfaces, extends and implements

A subclass says what it extends and what it overrides; an interface lists what several classes promise to have. shot check holds both to their word.

You'll learn extends, super, override, interfaces and implements.

Extends and override

A VIP wallet doubles every pickup. Today it is a second metatable and a function that happens to share a name; here the class says so:

struck ShotScript writes it for youhighlighted new in ShotScript
Luau todayVipWallet (a ModuleScript)
--!strictlocal Wallet = require(script.Parent.Wallet)local Types = require(script.Parent.Types)local VipWallet = {}VipWallet.__index = VipWalletsetmetatable(VipWallet, Wallet)export type VipWallet = Wallet.Walletfunction VipWallet.new(balance: number)	local self = Wallet.new(balance) :: any	return setmetatable(self, VipWallet)endfunction VipWallet.Add(self: VipWallet,	amount: number, reason: Types.Reason)	if reason == "Pickup" then		amount *= 2	end	Wallet.Add(self, amount, reason)endreturn VipWallet
ShotScript1916 linesWallet/VipWallet.shot
types script.Parent.Typesrootdoc { A wallet that doubles every pickup }class VipWallet extends Wallet = {	constructor new(balance : number)		local base = super.new(balance)		local self : VipWallet = init(base)		return self	end	override Add(amount : number,		reason : Reason)		if reason == "Pickup" then			amount *= 2		end		super:Add(amount, reason)	end}
What it compiles toWallet/VipWallet.luau
--!strict
local Types = require(script.Parent.Types)
export type Reason = Types.Reason
export type Entry = Types.Entry
export type Wallet = Types.Wallet
type WalletClass = Types.WalletClass
export type Bank = Types.Bank
export type Coins = Types.Coins
export type Purse = Types.Purse
export type VipWallet = Types.VipWallet
type VipWalletClass = Types.VipWalletClass
export type Whole = Types.Whole

local Wallet = require(script.Parent.Wallet)

--[[$rootdoc
	A wallet that doubles every pickup
]]
local VipWallet = {}
VipWallet.__index = VipWallet
setmetatable(VipWallet :: any, Wallet)


--[[$constructor]] function VipWallet.new(balance : number) : VipWallet
	local base = Wallet.new(balance)
	local self : VipWallet = setmetatable(base, VipWallet) :: any
	return self
end

--[[$override]] function VipWallet.Add(self : VipWallet, amount : number,
		reason : Reason)
	if reason == "Pickup" then
		amount *= 2
	end
	Wallet.Add(self, amount, reason)
end

return VipWallet
  • super is the parent class: super.new(balance) builds its object, and init(base) makes that object a VipWallet.
  • super:Add(amount, reason) runs the parent's own Add on this object: Wallet.Add(self, amount, reason).
  • A VipWallet has every member of Wallet: its type is Wallet & { its own members }.
  • An override keeps the parent's parameter and return types.

Tip The parent is in the same folder, so it is required for you; a parent in another folder needs a require in the file and an import in Types.shot.

Write public Add instead of override Add, and the checker says so instead of quietly replacing the parent's method:

$ shot check
./Wallet/VipWallet.shot:11   Add already exists in Wallet: mark it override
1 errors in 1 file (9 files checked, 2.3 s)

Interfaces

Bank should hold any kind of wallet. An interface lists what they all have, with no code; a class that implements it is checked member by member.

Luau todayTypes (a ModuleScript, part)
-- every wallet should match this
export type Purse = {
	Balance: number,
	Add: (self: any, amount: number,
		reason: Reason) -> (),
	Describe: (self: any) -> string,
}
ShotScriptWallet/Purse.shot
types script.Parent.Typesrootdoc { What every kind of wallet has }interface Purse = {	Balance : number	Add(amount : number, reason : Reason)	Describe() : string}
What it compiles toWallet/Types.luau (part)
local function __shotimplements__Wallet__Purse(value : Wallet) : Purse return value end

...

--[[
	What every kind of wallet has
]]
--[[$public]] export type Purse = {
	Balance : number,
	Add : (self : any, amount : number, reason : Reason) -> (),
	Describe : (self : any) -> string,
}
  • Wallet promises it on its first line: class Wallet implements Purse = {. implements comes after extends, with one or more interfaces.
  • An interface compiles to a type only: there is nothing to require. The __shotimplements__ line is the check.
  • Every file of the folder sees Purse; another folder imports it in its Types.shot.

Bank now keeps any Purse, and Upgrade swaps a player's wallet for a VIP one:

ShotScriptWallet/Bank.shot (part)
use Wallet
use VipWallet
use Whole

...

Wallets : {[Player] : Purse} = {}

...

doc { Swaps the wallet for a VIP one }
function Upgrade(player : Player)
	local wallet = module.Wallets[player]
	if not wallet then
		return
	end
	module.Wallets[player] =
		VipWallet.new(wallet.Balance)
end
LuauScenarios/Vip.luau
-- Vip: after an upgrade, a pickup counts twice and the balance is kept.
return function(world)
	local Bank = world.Server.Require("Wallet.Bank")
	world:Boot()
	local ana = world:AddPlayer("Ana")
	world:Step(0.1)
	Bank.Give(ana, 5)
	Bank.Upgrade(ana)
	Bank.Give(ana, 10)
	world.Logf("Ana has %s", Bank.Wallets[ana]:Describe())
	assert(Bank.Wallets[ana].Balance == 25, "5 + 2 x 10")
end
$ shot run Vip
==== Vip
[client:Ana print] client started
Ana has 25 coins

==== summary
  Vip            OK  (2 lines, 0 errors, 0.1s virtual)
1 of 1 scenario(s) passed

A broken promise

Add Spend(amount : number) : boolean to Purse and nothing else. Wallet no longer keeps its promise, and every place that hands a Wallet where a Purse goes is found too:

$ shot check
./Wallet/Bank.shot:16:28   TypeError: Table type 'Wallet' not compatible with type 'Purse' because the former is missing field 'Spend'
./Wallet/Bank.shot:30:16   TypeError: Expected this to be 'Purse', but got 't1 where t1 = { Add: (*CYCLE*, number, "Pickup" | "Shop") -> (), ... 3 more ... } & { Add: (t1, number, "Pickup" | "Shop") -> () }'; none of the intersection parts are compatible
./Wallet/Wallet.shot:18:14   Wallet implements Purse but has no Spend(amount : number) : boolean
3 errors in 2 files (9 files checked, 2.2 s)
Try it

Change the override's amount : number to amount : string and run shot check. The first line names both signatures: override Add is (string, Reason) -> () but Wallet.Add is (number, Reason) -> ().