Classes

Declare a class in one block, its fields, constructors and methods side by side, and ShotScript writes the metatable and the types for you.

A class file declares one class. Its fields and methods are public or internal, constructors build an object with init(), methods get self, and static functions belong to the class itself. This page covers each kind of member, then class properties, generic classes, inheritance, XClass (the type of the class table) and sides.

A class

A Shape with two constructors, two methods and a static function, and the type its second constructor takes:

Shapes/Shape.shot

types script.Parent.Types

local made = 0

class Shape = {
    doc { Shown in the UI }
    public Name : string
    internal _hp : number

    constructor new(name : string)
        local self : Shape = init()
        self.Name = name
        self._hp = 10
        made += 1
        return self
    end

    constructor FromStored(data : StoredShape)
        local self = Shape.new(data.Name)
        self._hp = data.Hp
        return self
    end

    public Damage(amount : number)
        self._hp -= amount
        if self:IsDead() then
            print(self.Name, "is gone")
        end
    end

    public IsDead() : boolean
        return self._hp <= 0
    end

    public static Made() : number
        return made
    end
}

Shapes/Types.shot

public type StoredShape = { Name : string, Hp : number }

Using it:

local Shape = require(@shot.Shapes.Shape)

local box = Shape.new("box")
box:Damage(3)
print(box:IsDead(), box.Name, Shape.Made())

Members

public Name : Typea field anyone may use
internal Name : Typea field only code in this folder may use
public Name : Type = valuea class property: a field with a default (see Class properties below)
constructor Name(params)creates an object: Shape.new(...)
public Name(params) : Returna method: object:Name(...)
internal Name(params) : Returna method only code in this folder may call
public static Name(params)a function of the class, no object: Shape.Name(...)
internal static Name(params)the same, folder-only
override Name(params) : Returnreplaces a parent's method (see Inheritance below)
doc { ... }documents the member below it (see Doc comments)

Anything else at member level is an error: "class Shape: expected public, internal, override, constructor or doc, got 'private'". There is no private; internal is the closest.

Fields

  • A field is a name and a type. Without a value, every object sets its own in the constructor.
  • A long type may span several lines. When several files use it, define it in Types.shot and use its name.
  • Class-wide state only the class uses (counters, caches) is a plain local above the class, like "made" above.

Class properties

A field written with a value is a class property:

class Icon = {
    public Type : string = "Icon"
    doc { Seconds a hover tween takes }
    public Speed : number = 10
    internal _sizes : {number} = { 16, 32 }
    public Name : string

    constructor new(name : string)
        local self : Icon = init()
        self.Name = name
        return self
    end

    public Slow()
        self.Speed = 5
    end
}
  • The value is the default for every object. It lives on the class table, and objects read it through __index: Icon.new("coins").Speed is 10.
  • An object may overwrite its own copy: self.Speed = 5 changes only that object.
  • Writing on the class changes the default for every object that hasn't overwritten it: Icon.Speed = 20.
  • It is typed on the object (icon.Speed) and on the class (Icon.Speed, in IconClass).
  • A default of the wrong type is an error at its line: public Speed : number = "fast" gives "Cannot cast 'string' into 'number' because the types are unrelated".
  • A typical use is a tag: public Type : "Badge" = "Badge".
  • A subclass may declare class properties of its own. Don't redeclare a parent's literal-typed property with another literal (parent Type : "Icon", child Type : "Fancy"): the child's type becomes "Fancy" & "Icon", which nothing fits ("Expected this to be '"Fancy" & "Icon"', but got '"Fancy"'"). When subclasses change it, type the parent's property wider (Type : string) and set it in the child's constructor.
  • Errors: "Speed: a class property needs a type and a value: Speed : type = value" (public Speed : number =), and "static is for functions only. A field with a value is already a class property: public Speed : type = value" for any public static X : T, with or without a value.

Constructors

  • constructor Name(params). Any name, and as many as you like: new, FromStored, Empty, ...
  • Called with a dot: Shape.new("box"), Shape.FromStored(data).
  • No return type: a constructor always returns its class. Writing one is an error ("constructor new: a constructor always returns Shape, so it takes no return type (drop : Shape)").
  • init() is short for setmetatable({}, Shape) :: any, a new empty object. Annotate the local so the body is checked: local self : Shape = init(). Then set every field that has no default, and return self.
  • init(x) makes x the object instead of an empty table: setmetatable(x, Shape) :: any. A subclass builds on its parent this way: local self : Square = init(super.new(size)), then sets its own fields.
  • A constructor may build on another one: local self = Shape.new(data.Name).
  • init() works only inside a class ("init() only works inside a class"). x.init() and "init()" in a string are left alone.
  • super, inside a class that extends another, is the parent class: super.new(...) calls its constructor, and super:Name(...) calls the parent's own Name on this object (it compiles to Parent.Name(self, ...)), so an override can run the parent's version first. Outside such a class it is an error ("super only works in a class that extends another: class X has no extends"), and naming a local super is refused.

Methods

  • Declared without self; inside the body, self is the object, typed as the class.
  • Called with a colon: box:Damage(3). Shape.Damage(box, 3) is the same call.
  • Inside the class, call other methods with self: self:IsDead().
  • A field and a method can't share a name ("Table type field 'Size' is a duplicate").

Static functions

  • public static Name(params) : Return, or internal static. No self.
  • Called on the class: Shape.Made(). Not on an object: statics are not part of the object's type (box.Made() is "Key 'Made' not found in table 'Shape'").
  • Use them for factories and class-wide queries.
  • public required static Name(params): the game must call it somewhere, or shot check reports "required Shape.Setup is never called: the game runs without it" (Modules, REQUIRED FUNCTIONS). Only statics can be required; "required is for static functions" otherwise.
  • A class can promise an interface's members: class Wallet implements Payer (Interfaces).
  • "override static" is refused: "override static: a static function belongs to its class and can't be overridden".

Parameters

  • Give every parameter a name and a type. An untyped parameter is any, and the checker then complains where it is used ("Unknown type used in + operation; consider adding a type annotation to 'x'").
  • "_" is a fine name for one you don't use, and may repeat: public Blank(_ : number, _ : string).
  • Don't name a parameter self (methods already have it: "DuplicateLocal: Function parameter 'self' already defined"), and don't use one name twice in a list.
  • A parameter named like the class hides the class inside that method (Shape.new is then not found).
  • Variadic parameters are written "...: T" (public Fire(...: any)).

Visibility

  • public: anyone.
  • internal: only code in the same folder: every file of that folder, subclasses in that folder included. Code in another folder that uses it is an error: "internal Shape._hp is used outside Shapes". That includes subclasses in other folders.
  • The check goes by member name: if any class or module anywhere has a public member with the same name (or overrides one with that name), uses of that name are not checked. Give internal members names nothing public uses.

Generic classes

A class may take type parameters, written after its name:

class Box<T> = {
    public Value : T

    constructor new(value : T)
        local self : Box<T> = init()
        self.Value = value
        return self
    end

    public Get() : T
        return self.Value
    end
}

T can be used in fields, parameters and return types.

  • A method or a static function may add its own type parameters, in any class:
public Map<U>(fn : (number) -> U) : {U}            bag:Map(tostring) is a {string}
public static Wrap<U>(value : U) : {U}
  • Inside Box<T>, the type Box<U> with another parameter is refused by Luau ("Recursive type being used with different parameters"), so a method can't return a Box of another type.

Inheritance

A Circle is a Shape with a radius, and its own idea of when it is dead:

Shapes/Circle.shot

types script.Parent.Types

class Circle extends Shape = {
    public Radius : number

    constructor new(name : string, radius : number)
        local self : Circle = setmetatable(Shape.new(name), Circle) :: any
        self.Radius = radius
        self._hp = 5
        return self
    end

    override IsDead() : boolean
        return self.Radius <= 0 or Shape.IsDead(self)
    end
}
  • The constructor builds the parent's object and re-tags it: setmetatable(Parent.new(...), Child) :: any.
  • A Circle has every member of Shape. Its type is Shape & { its own members }.
  • override Name(...) replaces a parent method. Its parameter types and return type must be the parent's (names may differ). Call the parent's version as Parent.Name(self, ...).
  • Errors: redefining a parent method without override ("IsDead already exists in Shape: mark it override"), an override with another signature ("override IsDead is (number) -> boolean but Shape.IsDead is () -> boolean"), an override of nothing ("override Fly: no Fly in Shape or above").
  • The parent's internal members are usable in the child only when both are in the same folder (self._hp above).

A parent in the same folder is required for you. A parent in another folder needs two lines from you: require it in the file, and import its type in your folder's Types.shot.

Zoo/Types.shot

import Shape from @shot.Shapes.Shape

Zoo/Square.shot

types script.Parent.Types

local Shape = require(@shot.Shapes.Shape)

class Square extends Shape = {
    constructor new()
        local self : Square = setmetatable(Shape.new("square"), Square) :: any
        return self
    end
}

Without the require: "extends Shape: it lives in another folder, so require it first (local Shape = require(...))". Without the import: "Unknown type 'Shape'; it is declared in Shapes: add import Shape from @shot.Shapes.Shape to Zoo/Types.shot".

XClass: the type of the class itself

Every class X also gets an internal type XClass: the type of the class table itself (the value require returns, as opposed to an object). It holds X's class properties, its constructors (returning X), its static functions, and its methods in dot form (self : X, ...) -> R.

Shapewhat an object has: fields and methods
ShapeClassnew : (name : string) -> Shape, FromStored : ..., Made : () -> number, Damage : (self : Shape, amount : number) -> (), IsDead : (self : Shape) -> boolean

Use it where code holds the class itself, for example a module that creates objects through a class it can swap:

Shapes/Factory.shot

types script.Parent.Types

local Shape = require(script.Parent.Shape)

module Factory = {
    Maker : ShapeClass = Shape

    function Make(name : string) : Shape
        return module.Maker.new(name)
    end
}
  • XClass is visible in every file of X's folder, like any type there.
  • It is internal: other folders can't import it ("import: ShapeClass is internal to @shot.Shapes.Shape (class types and internal types stay in their folder)"). Systems hand out objects through their public API.
  • A child's class type lists only the child's own members: CircleClass has Circle.new and Circle.IsDead, and Shape.FromStored and Shape.Made are on ShapeClass. What a child inherits still works through it (Circle.Made(), or through a CircleClass value) but comes back untyped (any).
  • Don't declare your own type or class called XClass while class X exists: "ShapeClass is the generated type of class Shape: pick another name".

Sides

server class Till = { ... }only server code may require it
client class Hud = { ... }only client code may require it
class Shape = { ... }shared ("both"): may only require shared code

A shared class that needs a server module must become a server class (see Server and client).