CoMaps

CoMaps are key-value objects that work like JavaScript objects. You can access properties with dot notation and define typed fields that provide TypeScript safety. They're ideal for structured data that needs type validation.

Creating CoMaps

CoMaps are typically defined by extending the CoMap class and specifying primitive fields using the co declarer (see Defining schemas: CoValues for more details on primitive fields):

class Project extends CoMap {
  name = co.string;
  startDate = co.Date;
  status = co.literal("planning", "active", "completed");
  coordinator = co.optional.ref(Member);
}

You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs:

class Inventory extends CoMap.Record(co.number) {}

To instantiate a CoMap:

const project = Project.create({
  name: "Spring Planting",
  startDate: new Date("2025-03-15"),
  status: "planning",
});

const inventory = Inventory.create({
  tomatoes: 48,
  basil: 12,
});

Ownership

When creating CoMaps, you can specify ownership to control access:

// Create with default owner (current user)
const const privateProject: ProjectprivateProject = class ProjectProject.
CoMap.create<Project>(this: CoValueClass<...>, init: {
    name: co<string> & (co<string> | undefined);
    startDate: co<Date> & (co<Date> | undefined);
    status: co<"planning" | "active" | "completed"> & (co<...> | undefined);
    coordinator?: Member | ... 2 more ... | undefined;
}, options?: {
    owner: Account | Group;
    unique?: CoValueUniqueness["uniqueness"];
} | Account | Group): Project
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.
@example```ts const person = Person.create({ name: "Alice", age: 42, pet: cat, }, { owner: friendGroup }); ```@categoryCreation
create
({
name: co<string> & (co<string> | undefined)name: "My Herb Garden", startDate: co<Date> & (co<Date> | undefined)startDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-04-01"),
status: co<"planning" | "active" | "completed"> & (co<"planning" | "active" | "completed"> | undefined)status: "planning", }); // Create with shared ownership const const gardenGroup: GroupgardenGroup = class Group
@categoryIdentity & Permissions
Group
.
Group.create<Group>(this: CoValueClass<Group>, options?: {
    owner: Account;
} | Account): Group
create
();
const gardenGroup: GroupgardenGroup.Group.addMember(member: Account, role: AccountRole): void (+1 overload)addMember(const memberAccount: AccountmemberAccount, "writer"); const const communityProject: ProjectcommunityProject = class ProjectProject.
CoMap.create<Project>(this: CoValueClass<...>, init: {
    name: co<string> & (co<string> | undefined);
    startDate: co<Date> & (co<Date> | undefined);
    status: co<"planning" | "active" | "completed"> & (co<...> | undefined);
    coordinator?: Member | ... 2 more ... | undefined;
}, options?: {
    owner: Account | Group;
    unique?: CoValueUniqueness["uniqueness"];
} | Account | Group): Project
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.
@example```ts const person = Person.create({ name: "Alice", age: 42, pet: cat, }, { owner: friendGroup }); ```@categoryCreation
create
(
{ name: co<string> & (co<string> | undefined)name: "Community Vegetable Plot", startDate: co<Date> & (co<Date> | undefined)startDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-03-20"),
status: co<"planning" | "active" | "completed"> & (co<"planning" | "active" | "completed"> | undefined)status: "planning", }, { owner: Account | Groupowner: const gardenGroup: GroupgardenGroup }, );

See Groups as permission scopes for more information on how to use groups to control access to CoMaps.

Reading from CoMaps

CoMaps can be accessed using familiar JavaScript object notation:

console.log(project.name);      // "Spring Planting"
console.log(project.status);    // "planning"

Handling Optional Fields

Optional fields require checks before access:

if (project.coordinator) {
  console.log(project.coordinator.name);  // Safe access
}

Working with Record CoMaps

For record-type CoMaps, you can access values using bracket notation:

const inventory = Inventory.create({
  tomatoes: 48,
  peppers: 24,
  basil: 12
});

console.log(inventory["tomatoes"]);  // 48

Updating CoMaps

Updating CoMap properties uses standard JavaScript assignment:

project.name = "Spring Vegetable Garden";    // Update name
project.startDate = new Date("2025-03-20");  // Update date

Type Safety

CoMaps are fully typed in TypeScript, giving you autocomplete and error checking:

project.name = "Spring Vegetable Planting";  // ✓ Valid string
project.startDate = "2025-03-15";  // ✗ Type error: expected Date

Deleting Properties

You can delete properties from CoMaps:

delete inventory["basil"];  // Remove a key-value pair

// For optional fields in struct-like CoMaps
project.coordinator = null;  // Remove the reference

Best Practices

Structuring Data

  • Use struct-like CoMaps for entities with fixed, known properties
  • Use record-like CoMaps for dynamic key-value collections
  • Group related properties into nested CoMaps for better organization

Common Patterns

Using Computed Properties

CoMaps support computed properties and methods:

class ComputedProject extends CoMap {
  name = co.string;
  startDate = co.Date;
  endDate = co.optional.Date;

  get isActive() {
    const now = new Date();
    return now >= this.startDate && (!this.endDate || now <= this.endDate);
  }

  formatDuration(format: "short" | "full") {
    const start = this.startDate.toLocaleDateString();
    if (!this.endDate) {
      return format === "full"
        ? `Started on ${start}, ongoing`
        : `From ${start}`;
    }

    const end = this.endDate.toLocaleDateString();
    return format === "full"
      ? `From ${start} to ${end}`
      : `${(this.endDate.getTime() - this.startDate.getTime()) / 86400000} days`;
  }
}

// ...

console.log(computedProject.isActive); // false
console.log(computedProject.formatDuration("short")); // "3 days"