Constructor Patterns #

PlexusInit & Constructor Shape #

Models accept a props object, used as an initialization structure. You have to declare them manually:

Why? TS is not capable to make constructor depend on instance fields.


@syncing("MyModel")
class MyModel extends PlexusModel {
  @syncing accessor name!: string;

  constructor(props: { name: string }) {
    super(props); // PlexusInit<this> | undefined
  }
}

Omittable Fields #

Nullable fields and collections can be omitted from constructors:


@syncing("Project")
class Project extends PlexusModel {
  @syncing accessor title!: string;                // Required
  @syncing accessor description!: string | null;   // Omittable (nullable)
  @syncing.list accessor tags!: string[];           // Omittable (spawns empty)
}

new Project({ title: "Hello" }); // Only title is required

Accessor Syntax #

Use !: Type | null for nullable fields. The = null initializer is equivalent:

class {
  // Both are equivalent:
  @syncing accessor owner!: User | null;
  @syncing accessor owner: User | null = null;
}

Inheritance #

@syncing is required on every level of the class hierarchy. Pass a string to set the model name (used for CRDT type maps and cross-peer resolution):


@syncing("SuperProject")
class SuperProject extends Project {
  // field types can be redefined in subclasses
  // @ts-expect-error - it IS typescript error, but we allow overwriting child to non-child vice-versa.
  @syncing.child accessor title: string | RichName = "";
}

The declare TS keyword enables type narrowing without adding syncing behavior:


@syncing("ConcreteGroup")
class ConcreteGroup extends AbstractGroup {
  declare items: SpecificItem[]; // Narrows type, no decorator needed
}