Step 4 of 14, updated

Classes

A class is one block: fields, constructor and methods side by side. The metatable, the self parameters and the types are written for you.

You'll learn fields, public and internal, constructors and init(), methods and self, statics.

A class

Wallet, one player's coins, both ways. It goes in Wallet/Wallet.shot, named like its folder, so it is the system's entry point: require(@shot.Wallet) returns it.

struck ShotScript writes it for youhighlighted new in ShotScript
Luau todayWallet (a ModuleScript)
--!strictlocal Types = require(script.Parent.Types)local Coins = require(script.Parent.Coins)local Wallet = {}Wallet.__index = Wallettype Fields = {	Balance: number,	_history: {Types.Entry},}export type Wallet = typeof(	setmetatable({} :: Fields, Wallet))function Wallet.new(balance: number): Wallet	return setmetatable({		Balance = balance,		_history = {},	}, Wallet)endfunction Wallet.Add(self: Wallet,	amount: number, reason: Types.Reason)	self.Balance += amount	table.insert(self._history,		{ Amount = amount, Reason = reason })endfunction Wallet.Describe(self: Wallet): string	return Coins.Format(self.Balance)endreturn Wallet
ShotScript2723 linesWallet/Wallet.shot
types script.Parent.Typesuse Coinsclass Wallet = {	public Balance : number	internal _history : {Entry}	constructor new(balance : number)		local self : Wallet = init()		self.Balance = balance		self._history = {}		return self	end	public Add(amount : number,		reason : Reason)		self.Balance += amount		table.insert(self._history, {			Amount = amount,			Reason = reason,		})	end	public Describe() : string		return Coins.Format(self.Balance)	end}
What it compiles toWallet/Wallet.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 Coins = Types.Coins

local Coins = require(script.Parent.Coins)

local Wallet = {}
Wallet.__index = Wallet

--@public Balance number
--@internal _history {Entry}


--[[$constructor]] function Wallet.new(balance : number) : Wallet
	local self : Wallet = setmetatable({},Wallet) :: any
	self.Balance = balance
	self._history = {}
	return self
end

--[[$public]] function Wallet.Add(self : Wallet, amount : number,
		reason : Reason)
	self.Balance += amount
	table.insert(self._history, {
		Amount = amount,
		Reason = reason,
	})
end

--[[$public]] function Wallet.Describe(self : Wallet) : string
	return Coins.Format(self.Balance)
end

return Wallet

Tip use Coins needs no path here: Coins.shot is in the same folder.

The members

  • public Balance anyone may use; internal _history only code in this folder. There is no private.
  • Every member starts with its word: public, internal, constructor or override. A bare Describe() : string is an error.
  • constructor new(...) makes an object, called with a dot: Wallet.new(10). Any name, as many as you need, no return type.
  • init() is setmetatable({}, Wallet) :: any. Write local self : Wallet = init(), set every field with no default, return self.
  • A method has no self parameter; inside it self is the object. Call it with a colon: wallet:Add(15, "Pickup").
  • public static Empty() : Wallet belongs to the class: Wallet.Empty().
  • public Limit : number = 1000 is a class property: the default each object reads until it sets its own.

The types it writes

The object's type (fields, then methods) and WalletClass, the class table's type:

LuauWallet/Types.luau (part)
--[[$public]] export type Wallet = {
	Balance : number,
	_history : {Entry},

	Add : (self : Wallet, amount : number, reason : Reason) -> (),
	Describe : (self : Wallet) -> string,
}

--[[$internal]] export type WalletClass = {
	new : (balance : number) -> Wallet,
	Add : (self : Wallet, amount : number, reason : Reason) -> (),
	Describe : (self : Wallet) -> string,
}

Using it

ShotScriptInit/Server.shot
@server enabled

use @shot.Wallet

local wallet = Wallet.new(10)
wallet:Add(15, "Pickup")
print(wallet:Describe())
$ shot run Hello
==== Hello
[server print] 25 coins
[client:Ana print] client started

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

internal is a rule the checker keeps. Add print(#wallet._history) to Init/Server.shot:

$ shot check
./Init/Server.shot:8:15   internal Wallet._history is used outside Wallet
1 errors in 1 file (5 files checked, 2.5 s)
Try it

Add public static Empty() : Wallet that returns Wallet.new(0) and call Wallet.Empty() from Init/Server.shot. Then call it on an object, wallet.Empty(): shot check answers Key 'Empty' not found in table 'Wallet', because statics belong to the class.