Nexus Instance is a utility for creating custom instances with property change events while still providing proper typing in Luau.
Classes with metatables are created mostly the same, except:
NexusInstance.ToInstance
is called before using or returning the class.- The recommended type of
self
becomesNexusInstance<Class>
.
The return type of NexusInstance.ToInstance
is
NexusInstanceClass<TClass, TConstructor>
, which should be
casted to. The generic types are:
TClass
: Type of the class without properties. Typically will betypeof(MyClass)
whereMyClass
is the table of the class.TConstructor
: Type of the constructor function. It should be the arguments ofnew(...)
with the return ofNexusInstance<MyClass>
.
--!strict
local NexusInstance = require(game:GetService("ReplicatedStorage"):WaitForChild("NexusInstance"))
local TestClass = {}
TestClass.__index = TestClass
--Exported test class type and Nexus Instance version (optional).
export type TestClass = {
TestProperty: string,
} & typeof(setmetatable({}, TestClass))
export type NexusInstanceTestClass = NexusInstance.NexusInstance<TestClass>
--Optional constructor when `new` is called.
function TestClass.__new(self: NexusInstanceTestClass, Argument: string): ()
self.TestProperty = Argument
end
--Custom function.
function TestClass.ChangeValue(self: NexusInstanceTestClass, NewValue: string): ()
self.TestProperty = NewValue
end
--Optional destroy function to clear resources.
--Events are cleaned internally (`Changed`, `GetPropertyChangedSignal`, and `CreateEvent` only).
--The version returned by NexusInstance.ToInstance will always have a Destroy method.
function TestClass.Destroy(self: NexusInstanceTestClass): ()
--Clear resources.
end
--Create the class to return in the ModuleScript, or use within the script.
--The constructor (second generic type) should match inputs for `__new` without the `self`.
local ReturnedTestClass = NexusInstance.ToInstance(TestClass) :: NexusInstance.NexusInstanceClass<typeof(TestClass), (Argument: string) -> (NexusInstanceTestClass)>
--Create and destroy the type.
local TestObject = ReturnedTestClass.new("TestValue")
print(TestObject.TestProperty) --"TestValue"
TestObject:ChangeValue("NewValue")
print(TestObject.TestProperty) --"NewValue"
TestObject:Destroy()
__new
and Destroy
are not required and can be omitted.
--!strict
local NexusInstance = require(game:GetService("ReplicatedStorage"):WaitForChild("NexusInstance"))
local TestClass = {}
TestClass.__index = TestClass
--Exported test class type and Nexus Instance version (optional).
export type TestClass = {
TestProperty: string?, --No constructor initializes this.
} & typeof(setmetatable({}, TestClass))
export type NexusInstanceTestClass = NexusInstance.NexusInstance<TestClass>
--Custom function.
function TestClass.ChangeValue(self: NexusInstanceTestClass, NewValue: string): ()
self.TestProperty = NewValue
end
--Create the class to return in the ModuleScript, or use within the script.
local ReturnedTestClass = NexusInstance.ToInstance(TestClass) :: NexusInstance.NexusInstanceClass<typeof(TestClass), () -> (NexusInstanceTestClass)>
--Create and destroy the type.
local TestObject = ReturnedTestClass.new()
print(TestObject.TestProperty) --nil
TestObject:ChangeValue("NewValue")
print(TestObject.TestProperty) --"NewValue"
TestObject:Destroy()
Inheritance is nearly the same as doing so with metatables, except for
the calls to NexusInstance.ToInstance
.
--!strict
local NexusInstance = require(game:GetService("ReplicatedStorage"):WaitForChild("NexusInstance"))
--Define TestClass1 (potentially in a ModuleScript).
local TestClass1 = {}
TestClass1.__index = TestClass1
export type TestClass1 = {
TestProperty1: string,
} & typeof(setmetatable({}, TestClass1))
export type NexusInstanceTestClass1 = NexusInstance.NexusInstance<TestClass1>
function TestClass1.__new(self: NexusInstanceTestClass1, Input: string)
self.TestProperty1 = Input
end
function TestClass1.ChangeValue1(self: NexusInstanceTestClass1, NewValue: string): ()
self.TestProperty1 = NewValue
end
local TestClass1NexusInstance = NexusInstance.ToInstance(TestClass1) :: NexusInstance.NexusInstanceClass<typeof(TestClass1), (Input: string) -> (NexusInstanceTestClass1)>
--Define TestClass2 (potentially in a different ModuleScript).
local TestClass2 = {}
TestClass2.__index = TestClass2
setmetatable(TestClass2, TestClass1NexusInstance) --TestClass1NexusInstance would be returned instead of TestClass1.
export type TestClass2 = {
TestProperty2: string,
} & typeof(setmetatable({}, TestClass2)) & TestClass1
export type NexusInstanceTestClass2 = NexusInstance.NexusInstance<TestClass2>
function TestClass2.__new(self: NexusInstanceTestClass2, Input1: string, Input2: string)
TestClass1.__new(self, Input1) --Remember to call the parent constructor!
self.TestProperty2 = Input2
end
function TestClass2.ChangeValue2(self: NexusInstanceTestClass2, NewValue: string): ()
self.TestProperty2 = NewValue
end
local TestClass2NexusInstance = NexusInstance.ToInstance(TestClass2) :: NexusInstance.NexusInstanceClass<typeof(TestClass2), (Input1: string, Input2: string) -> (NexusInstanceTestClass2)>
--Use the classes.
local TestObject1 = TestClass1NexusInstance.new("TestValue1")
print(TestObject1.TestProperty1) --"TestValue1"
TestObject1:ChangeValue1("NewValue1")
print(TestObject1.TestProperty1) --"NewValue1"
TestObject1:Destroy()
local TestObject2 = TestClass2NexusInstance.new("TestValue1", "TestValue2")
print(TestObject2.TestProperty1) --"TestValue1"
print(TestObject2.TestProperty2) --"TestValue2"
TestObject2:ChangeValue1("NewValue1")
TestObject2:ChangeValue2("NewValue2")
print(TestObject2.TestProperty1) --"NewValue1"
print(TestObject2.TestProperty2) --"NewValue2"
TestObject2:Destroy()
Classes can define some metatable methods, which will be passed
through. __index
and __newindex
are not supported for this.
--!strict
local NexusInstance = require(game:GetService("ReplicatedStorage"):WaitForChild("NexusInstance"))
local TestClass = {}
TestClass.__index = TestClass
--Exported test class type and Nexus Instance version (optional).
export type TestClass = typeof(setmetatable({}, TestClass))
export type NexusInstanceTestClass = NexusInstance.NexusInstance<TestClass>
--__tostring metatable method.
function TestClass.__tostring(self: NexusInstanceTestClass): ()
return "TestClass"
end
--Create the class to return in the ModuleScript, or use within the script.
local ReturnedTestClass = NexusInstance.ToInstance(TestClass) :: NexusInstance.NexusInstanceClass<typeof(TestClass), () -> (NexusInstanceTestClass)>
--Create and destroy the type.
local TestObject = ReturnedTestClass.new()
print(tostring(TestObject)) --"TestClass"
TestObject:Destroy()
Similar to normal Roblox Instance
s, there is a Changed
event
that is fired when any property changes, and GetPropertyChangedSignal
to listen to a specific property changing.
TestObject.Changed:Connect(function(PropertyName)
print(`Property {PropertyName} changed.`)
end)
TestObject:GetPropertyChangedSignal("TestProperty"):Connect(function()
print("PropertyName changed.")
end)
Changed events can be ignored using HidePropertyChanges
.
HideNextPropertyChange
can be used to only hide the next property change.
TestObject.Changed:Connect(function(PropertyName)
print(`Property {PropertyName} changed.`)
end)
TestObject:GetPropertyChangedSignal("TestProperty"):Connect(function()
print("PropertyName changed.")
end)
TestObject:HidePropertyChanges("TestProperty") --This makes it so changed events never invokes for TestProperty.
TestObject:HideNextPropertyChange("TestProperty") --This makes it so only the next changed event doesn't get invoked for TestProperty.
OnAnyPropertyChanged
and OnPropertyChanged
also exist. Unlike
the events, they will immediately invoke after a property change
(as opposed to waiting on deferred events). They do not respect
hidden changed events and will always be invoked.
TestObject:OnAnyPropertyChanged(function(PropertyName, Value)
print(`Property {PropertyName} changed to {Value}.`)
end)
TestObject:OnPropertyChanged("TestProperty", function(Value)
print(`TestProperty changed to {Value}.`)
end)
When a property is set, it is able to be transformed before being stored and invoked with changed events. Generic transforms will always run before property-speicifc ones.
TestObject:AddGenericPropertyTransform(function(Index, Value)
return `{Value}_{Index}_1`
end)
TestObject:OnAnyPropertyChanged("TestProperty", function(Value)
return `{Value}_2`
end)
TestObject.TestProperty = "NewValue"
print(TestObject.TestProperty) --"NewValue_TestProperty_1_2"
TypedEvent
exists for custom events. Compared to BindableEvent
:
TypedEvent
s have typing for the arguments.- Arguments that are passed retain their original table/function references, instead of being encoded away.
CreateEvent
is always recommended unless being used outside
of an instance, since CreateEvent
will handle disconnecting
the event when the instance is destroyed.
--!strict
local NexusInstance = require(game:GetService("ReplicatedStorage"):WaitForChild("NexusInstance"))
local TestClass = {}
TestClass.__index = TestClass
--Exported test class type and Nexus Instance version (optional).
export type TestClass = {
TestEvent: NexusInstance.TypedEvent<string>,
} & typeof(setmetatable({}, TestClass))
export type NexusInstanceTestClass = NexusInstance.NexusInstance<TestClass>
--Optional constructor when `new` is called.
function TestClass.__new(self: NexusInstanceTestClass): ()
self.TestEvent = self:CreateEvent() :: NexusInstance.TypedEvent<string>
end
--Create the class to return in the ModuleScript, or use within the script.
local ReturnedTestClass = NexusInstance.ToInstance(TestClass) :: NexusInstance.NexusInstanceClass<typeof(TestClass), () -> (NexusInstanceTestClass)>
--Create and destroy the type.
local TestObject = ReturnedTestClass.new()
TestObject.TestEvent:Connect(function(Message)
print(Message)
end)
TestObject.TestEvent:Fire("Test message") --Prints "Test message"
TestObject:Destroy() --Disconnects TestEvent.
Both issues and pull requests are accepted for this project.
Nexus Instance is available under the terms of the MIT License. See LICENSE for details.