Attribute Module

Introduction

Hi. The purpose of this module is to remove some of the boilerplate involved with using Instance Attributes in scripts and custom metatables. Instead of using :GetAttribute() and :SetAttribute() everywhere, this module allows you to get and set attributes more conventionally while also reflecting any outside changes to the Attribute into the class state. This module is also strictly-typed so you get all of the code-completion, hints, and other weird stuff.

Usage & Examples

Creating an Attribute

After being required, a new Attribute can be created with .new(Instance, Name, Value).

local Attribute = require(ReplicatedStorage.Attribute)

local Health = Attribute.new(workspace.Part, "Health", 20)

In this example, an Attribute is being created on workspace.Part, with the name "Health" and the initial value set to 20. Passing an instance that already has an Attribute with a matching name and value type will also not set the initial value, so you can still use this with Attributes that have been created in Studio while preserving their set value.

Getting and setting Attributes

You can get and set the Attribute’s value by only using the .Value property! Any assignments to this property implicitly set the actual value of the Instance’s Attribute and throw an error if the value assigned does not match the type of the Attribute.

local Health = Attribute.new(workspace.Part, "Health", 20)

print(Health.Value) -- Prints 20.

Health.Value = 100 -- Trying to assign a boolean to this value would error.
print(Health.Value) -- Now prints 100!

Detecting changes

This class has a :GetChangedSignal() method that simply returns the Instance:GetAttributeChangedSignal(Name) signal of the Instance so you can hook into changes. You could also use the .Changed property.

Health.Changed:Connect(function()
	local Humanoid = workspace.Part:FindFirstChildWhichIsA("Humanoid")
	
	if not Humanoid then
		return
	end
	
	Humanoid.MaxHealth += Health.Value
end)

Everything together

I’ve used this module personally for a while now, mostly in custom classes and Knit Components. Here’s sort of what that would look like.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Attribute = require(ReplicatedStorage.Shared.Attribute)

local Stats = {}
Stats.__index = Stats

function Stats.new(Entity)
	local self = setmetatable({}, Stats)
	
	self.Instance = Entity
	self.Health = Attribute.new(self.Instance, "Health", 100)
	
	self.Health:GetChangedSignal():Connect(function()
		local Humanoid = self.Instance:FindFirstChildWhichIsA("Humanoid")

		if not Humanoid then
			return
		end

		Humanoid.MaxHealth += self.Health.Value
	end)
	
	return self
end

function Stats:ReduceHP(Amount)
	self.Health.Value -= Amount
end

return Stats

Assets

The module is available on the Marketplace here. The code is just under 70 lines so I’ll also post it below.

Source
--!strict

local Attribute = {}

type self<T> = {
	Instance: Instance,
	Name: string,
	Value: T,
	
	Changed: RBXScriptSignal,
	
	GetChangedSignal: (self: self<T>) -> RBXScriptSignal
} & {[any]: any}

export type Attribute<T> = typeof(
	setmetatable({}::self<T>, Attribute)
)

function CompareTypes(Value1: unknown, Value2: unknown): boolean
	return typeof(Value1) == typeof(Value2)
end

function Attribute.new<T>(Instance: Instance, Name: string, Default: T): Attribute<T>
	local self = setmetatable({}::self<T>, Attribute)

	self.Instance = Instance
	self.Name = Name
	
	-- https://devforum.roblox.com/t/attribute-module/2784809/8
	self.Changed = self.Instance:GetAttributeChangedSignal(self.Name)
	
	self.Inner = Default
	
	self.GetChangedSignal = function(self: self<T>)
		return self.Changed
	end
	
	local ExistingAttribute = Instance:GetAttribute(Name)
	if not CompareTypes(Default, ExistingAttribute) then
		Instance:SetAttribute(Name, Default)
	end

	return self
end

function Attribute:__newindex(Index: unknown, Value: any): ()
	if Index == "Value" then
		if not CompareTypes(self.Inner, Value) then
			error(
				`Attempt to set attribute of type '{typeof(self.Inner)}' to type '{typeof(Value)}'`, 
				2
			)
			return
		end
		
		self.Instance:SetAttribute(self.Name, Value)
		
		return
	end
	
	rawset(self, Index, Value)
end

function Attribute.__index<T>(self: self<T>, Index: unknown): T
	if Index == "Value" then
		return self.Instance:GetAttribute(self.Name)
	end
	
	return rawget(self, Index)
end

return Attribute
9 Likes

In what way is this better than the regular attribute functions? Because to be honest it seems useless

1 Like

This is less painful than continuously putting :SetAttribute() and :GetAttribute()

I don’t want to write the same function over and over again. and this module solves that issue

1 Like

So it just saves a few letters? I don’t see any benefits in that, MAYBE if you used attributes for everything but that seems unlikely

I find it strange that you have to call GetChangedSignal to get the signal of when does attribute change.
Are there any issues doing the self.Changed = self.Instance:GetAttributeChangedSignal(self.Name) or it’s just your first thought that came into your mind?

3 Likes

@VSCPlays sort of nails most of the use-case here. It’s to avoid boilerplate code.
It also stores the attribute’s state within the class and updates to reflect the Attribute’s value if it is modified outside of the script.

1 Like

I have always had pain in writing the same functions. It’s just cleaner to have a single property for an attribute instead of putting two arguments of one being the name and the other being the value. Roblox does the same thing to properties and children. so why not have it for attributes?

@vvv331 It would’ve been better if you modify the code,

Replace the GetChangedSignal: (self: self<T>) -> RBXScriptSignal with GetChangedSignal: RBXScriptSignal and replace the

function(self: self<T>)
		return self.Instance:GetAttributeChangedSignal(self.Name)
	end

with self.Instance:GetAttributeChangedSignal(self.Name)

1 Like

I wrote it this way because of how detecting changes on Instances with attributes uses :GetAttributeChangedSignal() instead of a .Changed property and I didn’t even think about doing it like this.

I think I like your suggestion though so I’ll probably add it! Thanks!

EDIT: Added! There is now a .Changed property that returns the signal.

You should add lifetime for attribute, like it will automaticly removes. It’ll make them less useless

Thank you, I found this very useful as I am currently been trying to create custom typechecked lua objects!

1 Like