Skip to content

Properties

Heliguy edited this page May 23, 2026 · 2 revisions

GObject properties are the primary way to add state on a GObject subclass. They're more than just fields, as they can be interacted with in UI files, participate in GObject's binding system, and emit notify signals when changed.

Background - Properties in Plain GJS

In plain GJS, declaring a property requires verbose syntax within the GObject.registerClass config object, via the ParamSpec interface. Take this example for a subclass with title and count properties.

class MyBox extends Gtk.Box {
	static {
		GObject.registerClass({
			// string property that can be modified, and has an empty string as the default value
			"title": GObject.ParamSpec.string(
				"title", "", "",
				GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
				"",
			),
			// number property that can be modified, and has 0 as the default value
			"count": GObject.ParamSpec.uint(
				"count", "", "",
				GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
				0, GLib.MAXUINT32, 0,
			),
		}, this)
	}

	declare title: string
	declare count: number
}

Every property requires a verbose ParamSpec call that has repetitive use of the property name, and TypeScript needs an unsafe declare variable for each property as well, so that you may use them as fields on the class.

The GObjectify Way

GObjectify handles all of this through the Property object's descriptor functions, which are used in tandem with from().

@GClass()
class MyBox extends from({
	title: Property.readwrite.string(),
	count: Property.readwrite.uint32(),
}) {}

This is the same exact class, but as you can see, there is a lot less repetition, and everything is fully typed.
GObjectify automatically passes these declared properties to the register call, and adds getters and setters for numeric properties that clamp values to the min and max (GObjectify has the correct default min and max for each type).

Important Note about Defaults

If your subclass is using a template UI file, always use the default_value argument of the property's function. If a default value is supplied in the UI file, it will be ignored and thrown out. This is due to how GtkBuilder works. However, it is perfectly fine to provide values in other UI files that instantiate your subclass, as these use the constructor parameter correctly.

Property Types

String

title: Property.readwrite.string(),
title: Property.readwrite.string("Hello, World!"),

Backed by GObject.ParamSpec.string, and defaults to "".

Booleans

enabled: Property.readwrite.bool(),
enabled: Property.readwrite.bool(true),

Backed by GObject.ParamSpec.boolean, and defaults to false.

Numbers

GObjectify provides three numeric property types corresponding to underlying GTypes in C. GObjectify only exposes numeric types with ranges that fit safely within JS's number (no int64 for example).

Function GObject Type Default Range Use For
.int32() gint MININT32 to MAXINT32 Signed Integers
.uint32() guint 0 to MAXUINT32 Unsigned Integers, Counts
.double() gdouble -MAX_VALUE to MAX_VALUE Floats, maps directly to JS number
Property.readwrite.uint32()
Property.readwrite.uint32(5, { min: 0, max: 100 })

Property.readwrite.int32()
Property.readwrite.int32(0, { min: -100, max: 100 })

Property.readwrite.double()
Property.readwrite.double( 0.5, { min: 0.0, max: 1.0 })

All three support min and max config options. Values are automatically clamped to the range at runtime, something GJS doesn't do.
The default value is 0 if 0 fits within your range, else it's the specified min value, and min and max default to the respective type's minimum and maximum values.

GObject Instances

content: Property.readwrite.gobject(Gtk.Widget)

Backed by GObject.ParamSpec.object, and defaults to null, which cannot be changed. The value is always nullable.

GObject Enums

orientation: Property.readwrite.genum(Gtk.Orientation, Gtk.Orientation.VERTICAL)

Backed by GObject.ParamSpec.enum. Unlike other properties, genum properties require a default value to be passed. This is because GObject's enums do not guarantee that 0 is a valid value.

JavaScript Objects

data: Property.readwrite.jsobject()
data: Property.readwrite.jsobject().as<string[]>()

Backed by GObject.ParamSpec.jsobject, and defaults to null which cannot be changed. The value is always nullable.

Modifiers

Every property requires a mutability modifier to be specified. Under the hood these map to GObject.ParamFlags, but GObjectify gives them TypeScript-native names and behavior.

Property.readwrite

count: Property.readwrite.string()

GObject equivalent: GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT

Readable and writeable at all times. The CONSTRUCT flag is included so that the property is available in the constructor (see the Initialization Order page for why this matters).

Property.readonly

id: Property.readonly.string()

GObject equivalent: GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT

Despite the identical ParamFlags, properties of this modifier appear readonly in TypeScript. The property is enforced as readonly on the instance, but is allowed to be set through constructor arguments (including super()).

GObjectify adds a setter that will error upon a value being set, to enforce the readonly-ness of these properties.

Property.const

version: Property.const.string(pkg.version)

GObject equivalent: GObject.ParamFlags.READABLE

Truly readonly at the GObject level. The property is only allowed to be its default value, and can never change. Use this for fixed metadata that never changes.

Since Property.gobject and Property.jsobject cannot have their defaults changed, they are not allowed to be made via const. This is because they would always be null no matter what.

Property.computed

display_name: Property.computed.string()

GObject equivalent: GObject.ParamFlags.READWRITE

Unlike the other modifiers, computed is not only about mutability, but also allows for overriding the property through getters and setters, to allow for derived values. With any other modifier, GObjectify manages the accessors for you, but with computed, you are required to provide a getter and setter on the class body. genum cannot be computed, and computed properties cannot have default values.

Here is an example:

@GClass()
class MyWidget extends from(Gtk.Box, {
	display_name: Property.computed.string(),
}) {
	#first_name = ""
	#last_name = ""

	override get display_name(): string {
		return `${this.#first_name} ${this.#last_name}`
	}
	@Notify // emit the "notify::display-name" signal when the setter is called
	override set display_name(v: string) {
		const [this.#first_name, this.#last_name] = v.split(" ")
	}
}

Computed properties will be available after initialization finishes. But, because computed properties are not marked as CONSTRUCT, they cannot be passed to super().

Computed properties are not allowed to be set during subclass initialization time. This means that computed properties cannot be written to through UI files, as that triggers an "early write". An explicit check is added, which throws a runtime error if a computed property is written to during this time.

Before the subclass is fully ready, computed properties will return their default values for any reads that happen early. When the parent class initialization is fully complete, and control is handed to your constructor, only then will computed properties be able to run their getters and setters without erroring.

For more information, please see the Initialization Order page.

Note that not providing accessors will result in a runtime error at class definition, as will providing accessors for non-computed properties.

Type Narrowing

All property types support the .as suffix method, which utilizes generics to narrow the type. This is useful for things like strings and objects, which you might want to narrow to a specific set as opposed to all kinds.

// Narrow a string property to a specific union of possible values
status: Property.readwrite.string("active").as<"active" | "inactive" | "pending">(),

// Narrow a gobject property to a specific kind of widget
child: Property.readwrite.gobject(Gtk.Widget).as<Gtk.Image | Gtk.Picture>(),

// Narrow a jsobject property to a specific object type
data: Property.readwrite.jsobject().as<string[]>()

These narrowed types are enforced in the TypeScript type system, but are not enforced at runtime, so be mindful, especially when using UI files.

Clone this wiki locally