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.
Balance, Add, Describe: no codeimplementsextendsoverride Add doubles every pickup Bank keeps any Purse: a Wallet or a VipWallet.
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:
--!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
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
superis the parent class:super.new(balance)builds its object, andinit(base)makes that object a VipWallet.super:Add(amount, reason)runs the parent's ownAddon 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:
./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.
-- every wallet should match this export type Purse = { Balance: number, Add: (self: any, amount: number, reason: Reason) -> (), Describe: (self: any) -> string, }
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 = {.implementscomes afterextends, 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 itsTypes.shot.
Bank now keeps any Purse, and Upgrade swaps a player's wallet for a VIP one:
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
-- 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
==== 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:
./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)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) -> ().
