Defining schemas: CoValues
CoValues ("Collaborative Values") are the core abstraction of Jazz. They're your bread-and-butter datastructures that you use to represent everything in your app.
As their name suggests, CoValues are inherently collaborative, meaning multiple users and devices can edit them at the same time.
Think of CoValues as "super-fast Git for lots of tiny data."
- CoValues keep their full edit histories, from which they derive their "current state".
- The fact that this happens in an eventually-consistent way makes them CRDTs.
- Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's edit metadata.
CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams.
Start your app with a schema
Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app.
This helps correctness and development speed, but is particularly important...
- when you evolve your app and need migrations
- when different clients and server workers collaborate on CoValues and need to make compatible changes
Thinking about the shape of your data is also a great first step to model your app.
Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other.
In Jazz, you define schemas using co for CoValues and z (from Zod) for their primitive fields.
// schema.ts import {import coco,import zz } from "jazz-tools"; constconst ListOfTasks: co.List<z.z.ZodString>ListOfTasks =import coco.list(list<z.z.ZodString>(element: z.z.ZodString): co.List<z.z.ZodString> export listimport zz.string()); export constfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringTodoProject =const TodoProject: co.Map<{ title: z.z.ZodString; tasks: co.List<z.z.ZodString>; }, unknown, Account | Group>import coco.map({map<{ title: z.z.ZodString; tasks: co.List<z.z.ZodString>; }>(shape: { title: z.z.ZodString; tasks: co.List<z.z.ZodString>; }): co.Map<{ title: z.z.ZodString; tasks: co.List<z.z.ZodString>; }, unknown, Account | Group> export maptitle: z.z.ZodStringtitle:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringtasks: co.List<z.z.ZodString>tasks:const ListOfTasks: co.List<z.z.ZodString>ListOfTasks, });
This gives us schema info that is available for type inference and at runtime.
Check out the inferred type of project in the example below, as well as the input .create() expects.
// app.ts import {class GroupGroup } from "jazz-tools"; import {TodoProject,const TodoProject: CoMapSchema<{ title: ZodString; tasks: CoListSchema<ZodString>; }, unknown, Group | Account>const ListOfTasks: CoListSchema<ZodString>ListOfTasks } from "./schema"; constproject =const project: { readonly title: string; readonly tasks: CoList<string>; } & CoMapTodoProject.const TodoProject: CoMapSchema<{ title: ZodString; tasks: CoListSchema<ZodString>; }, unknown, Group | Account>create( {CoMapSchema<{ title: ZodString; tasks: CoListSchema<ZodString>; }, unknown, Group | Account>.create(init: { title: string; tasks: readonly string[] | CoList<string>; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)title: stringtitle: "New Project",tasks: readonly string[] | CoList<string>tasks:const ListOfTasks: CoListSchema<ZodString>ListOfTasks.create([],CoListSchema<ZodString>.create(items: readonly string[], options?: { owner: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): CoListInstance<ZodString> (+1 overload)class GroupGroup.create()), },Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupclass GroupGroup.create() );Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Group
When creating CoValues that contain other CoValues, you can pass in a plain JSON object. Jazz will automatically create the CoValues for you.
// app.ts import {class GroupGroup } from "jazz-tools"; import {TodoProject,const TodoProject: CoMapSchema<{ title: ZodString; tasks: CoListSchema<ZodString>; }, unknown, Group | Account>const ListOfTasks: CoListSchema<ZodString>ListOfTasks } from "./schema"; constconst group: Groupgroup =class GroupGroup.create().Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): GroupGroup.makePublic(role?: "reader" | "writer"): GroupMake the group public, so that everyone can read it. Alias for `addMember("everyone", role)`.makePublic(); constproject =const project: { readonly title: string; readonly tasks: CoList<string>; } & CoMapTodoProject.const TodoProject: CoMapSchema<{ title: ZodString; tasks: CoListSchema<ZodString>; }, unknown, Group | Account>create({CoMapSchema<{ title: ZodString; tasks: CoListSchema<ZodString>; }, unknown, Group | Account>.create(init: { title: string; tasks: CoList<string> | readonly string[]; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)title: stringtitle: "New Project",tasks: CoList<string> | readonly string[]tasks: [], // Permissions are inherited, so the tasks list will also be public },const group: Groupgroup);
To learn more about how permissions work when creating nested CoValues with plain JSON objects, refer to Ownership on implicit CoValue creation.
Types of CoValues
CoMap (declaration)
CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects (Collaborative editing follows a last-write-wins strategy per-key).
You can either declare struct-like CoMaps:
constTask =const Task: co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>import coco.map({map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }>(shape: { title: z.z.ZodString; completed: z.z.ZodBoolean; }): co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group> export maptitle: z.z.ZodStringtitle:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringcompleted: z.z.ZodBooleancompleted:import zz.boolean(), });function boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean export boolean
Or record-like CoMaps (key-value pairs, where keys are always string):
constconst ColorToHex: co.Record<z.z.ZodString, z.z.ZodString>ColorToHex =import coco.record(record<z.z.ZodString, z.z.ZodString>(_keyType: z.z.ZodString, valueType: z.z.ZodString): co.Record<z.z.ZodString, z.z.ZodString> export recordimport zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringimport zz.string()); constfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringColorToFruit =const ColorToFruit: co.Record<z.z.ZodString, co.Map<{ name: z.z.ZodString; color: z.z.ZodString; }, unknown, Account | Group>>import coco.record(record<z.z.ZodString, co.Map<{ name: z.z.ZodString; color: z.z.ZodString; }, unknown, Account | Group>>(_keyType: z.z.ZodString, valueType: co.Map<{ name: z.z.ZodString; color: z.z.ZodString; }, unknown, Account | Group>): co.Record<...> export recordimport zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringFruit);const Fruit: co.Map<{ name: z.z.ZodString; color: z.z.ZodString; }, unknown, Account | Group>
See the corresponding sections for creating, subscribing/loading, reading from and updating CoMaps.
CoList (declaration)
CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.)
You define them by specifying the type of the items they contain:
constconst ListOfColors: co.List<z.z.ZodString>ListOfColors =import coco.list(list<z.z.ZodString>(element: z.z.ZodString): co.List<z.z.ZodString> export listimport zz.string()); constfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringListOfTasks =const ListOfTasks: co.List<co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>>import coco.list(list<co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>>(element: co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>): co.List<...> export listTask);const Task: co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>
See the corresponding sections for creating, subscribing/loading, reading from and updating CoLists.
CoFeed (declaration)
CoFeeds are a special CoValue type that represent a feed of values for a set of users/sessions (Each session of a user gets its own append-only feed).
They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc.
You define them by specifying the type of feed item:
constFeedOfTasks =const FeedOfTasks: co.Feed<co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>>import coco.feed(feed<co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>>(element: co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>): co.Feed<...> export feedTask);const Task: co.Map<{ title: z.z.ZodString; completed: z.z.ZodBoolean; }, unknown, Account | Group>
See the corresponding sections for creating, subscribing/loading, reading from and writing to CoFeeds.
FileStream (declaration)
FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.)
They allow you to upload and reference files.
You typically don't need to declare or extend them yourself, you simply refer to the built-in co.fileStream() from another CoValue:
constDocument =const Document: co.Map<{ title: z.z.ZodString; file: co.FileStream; }, unknown, Account | Group>import coco.map({map<{ title: z.z.ZodString; file: co.FileStream; }>(shape: { title: z.z.ZodString; file: co.FileStream; }): co.Map<{ title: z.z.ZodString; file: co.FileStream; }, unknown, Account | Group> export maptitle: z.z.ZodStringtitle:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringfile: co.FileStreamfile:import coco.fileStream(), });function fileStream(): co.FileStream export fileStream
See the corresponding sections for creating, subscribing/loading, reading from and writing to FileStreams.
Note: For images, we have a special, higher-level co.image() helper, see ImageDefinition.
Unions of CoMaps (declaration)
You can declare unions of CoMaps that have discriminating fields, using co.discriminatedUnion().
constButtonWidget =const ButtonWidget: co.Map<{ type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }, unknown, Account | Group>import coco.map({map<{ type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }>(shape: { type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }): co.Map<{ type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }, unknown, Account | Group> export maptype: z.z.ZodLiteral<"button">type:import zz.literal("button"),literal<"button">(value: "button", params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<"button"> (+1 overload) export literallabel: z.z.ZodStringlabel:import zz.string(), }); constfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringSliderWidget =const SliderWidget: co.Map<{ type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }, unknown, Account | Group>import coco.map({map<{ type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }>(shape: { type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }): co.Map<{ type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }, unknown, Account | Group> export maptype: z.z.ZodLiteral<"slider">type:import zz.literal("slider"),literal<"slider">(value: "slider", params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<"slider"> (+1 overload) export literalmin: z.z.ZodNumbermin:import zz.number(),function number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export numbermax: z.z.ZodNumbermax:import zz.number(), }); constfunction number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export numberWidgetUnion =const WidgetUnion: co.DiscriminatedUnion<[co.Map<{ type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }, unknown, Account | Group>, co.Map<{ type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }, unknown, Account | Group>]>import coco.discriminatedUnion("type", [discriminatedUnion<[co.Map<{ type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }, unknown, Account | Group>, co.Map<{ type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }, unknown, Account | Group>]>(discriminator: string, schemas: [...]): co.DiscriminatedUnion<...> export discriminatedUnionButtonWidget,const ButtonWidget: co.Map<{ type: z.z.ZodLiteral<"button">; label: z.z.ZodString; }, unknown, Account | Group>SliderWidget]);const SliderWidget: co.Map<{ type: z.z.ZodLiteral<"slider">; min: z.z.ZodNumber; max: z.z.ZodNumber; }, unknown, Account | Group>
See the corresponding sections for creating, subscribing/loading and narrowing schema unions.
CoValue field/item types
Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain.
Primitive fields
You can declare primitive field types using z (re-exported in jazz-tools from Zod):
import {import coco,import zz } from "jazz-tools"; constPerson =const Person: co.Map<{ title: z.z.ZodString; }, unknown, Account | Group>import coco.map({map<{ title: z.z.ZodString; }>(shape: { title: z.z.ZodString; }): co.Map<{ title: z.z.ZodString; }, unknown, Account | Group> export maptitle: z.z.ZodStringtitle:import zz.string(), }) export constfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringconst ListOfColors: co.List<z.z.ZodString>ListOfColors =import coco.list(list<z.z.ZodString>(element: z.z.ZodString): co.List<z.z.ZodString> export listimport zz.string());function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string
Here's a quick overview of the primitive types you can use:
import zz.string(); // For simple stringsfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringimport zz.number(); // For numbersfunction number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export numberimport zz.boolean(); // For booleansfunction boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean export booleanimport zz.date(); // For datesfunction date(params?: string | z.z.core.$ZodDateParams): z.z.ZodDate export dateimport zz.literal(["waiting", "ready"]); // For enumsliteral<readonly ["waiting", "ready"]>(value: readonly ["waiting", "ready"], params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<"waiting" | "ready"> (+1 overload) export literal
Finally, for more complex JSON data, that you don't want to be collaborative internally (but only ever update as a whole), you can use more complex Zod types.
For example, you can use z.object() to represent an internally immutable position:
constSprite =const Sprite: co.Map<{ position: z.z.ZodObject<{ x: z.z.ZodNumber; y: z.z.ZodNumber; }, z.z.core.$strip>; }, unknown, Account | Group>import coco.map({ // assigned as a wholemap<{ position: z.z.ZodObject<{ x: z.z.ZodNumber; y: z.z.ZodNumber; }, z.z.core.$strip>; }>(shape: { position: z.z.ZodObject<{ x: z.z.ZodNumber; y: z.z.ZodNumber; }, z.z.core.$strip>; }): co.Map<...> export mapposition:position: z.z.ZodObject<{ x: z.z.ZodNumber; y: z.z.ZodNumber; }, z.z.core.$strip>import zz.object({function object<{ x: z.z.ZodNumber; y: z.z.ZodNumber; }>(shape?: { x: z.z.ZodNumber; y: z.z.ZodNumber; } | undefined, params?: string | { error?: string | z.z.core.$ZodErrorMap<NonNullable<z.z.core.$ZodIssueInvalidType<unknown> | z.z.core.$ZodIssueUnrecognizedKeys>> | undefined; message?: string | undefined; } | undefined): z.z.ZodObject<...>x: z.z.ZodNumberx:import zz.number(),function number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export numbery: z.z.ZodNumbery:import zz.number() }), });function number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export number
Or you could use a z.tuple():
constSprite =const Sprite: co.Map<{ position: z.z.ZodTuple<[z.z.ZodNumber, z.z.ZodNumber], null>; }, unknown, Account | Group>import coco.map({ // assigned as a wholemap<{ position: z.z.ZodTuple<[z.z.ZodNumber, z.z.ZodNumber], null>; }>(shape: { position: z.z.ZodTuple<[z.z.ZodNumber, z.z.ZodNumber], null>; }): co.Map<{ position: z.z.ZodTuple<[z.z.ZodNumber, z.z.ZodNumber], null>; }, unknown, Account | Group> export mapposition: z.z.ZodTuple<[z.z.ZodNumber, z.z.ZodNumber], null>position:import zz.tuple([tuple<[z.z.ZodNumber, z.z.ZodNumber]>(items: [z.z.ZodNumber, z.z.ZodNumber], params?: string | z.z.core.$ZodTupleParams): z.z.ZodTuple<[z.z.ZodNumber, z.z.ZodNumber], null> (+2 overloads) export tupleimport zz.number(),function number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export numberimport zz.number()]), });function number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export number
References to other CoValues
To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other.
Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert.
The important caveat here is that a referenced CoValue might or might not be loaded yet, but we'll see what exactly that means in Subscribing and Deep Loading.
In Schemas, you declare references by just using the schema of the referenced CoValue:
// schema.ts constPerson =const Person: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>import coco.map({map<{ name: z.z.ZodString; }>(shape: { name: z.z.ZodString; }): co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> export mapname: z.z.ZodStringname:import zz.string(), }); constfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringListOfPeople =const ListOfPeople: co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>import coco.list(list<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>(element: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>): co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>> export listPerson); constconst Person: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>Company =const Company: co.Map<{ members: co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>; }, unknown, Account | Group>import coco.map({map<{ members: co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>; }>(shape: { members: co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>; }): co.Map<...> export mapmembers:members: co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>ListOfPeople, });const ListOfPeople: co.List<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>
Optional References
You can make schema fields optional using either z.optional() or co.optional(), depending on the type of value:
- Use
z.optional()for primitive Zod values likez.string(),z.number(), orz.boolean() - Use
co.optional()for CoValues likeco.map(),co.list(), orco.record()
You can make references optional with co.optional():
constPerson =const Person: co.Map<{ age: z.ZodOptional<z.z.ZodNumber>; pet: co.Optional<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>; }, unknown, Account | Group>import coco.map({map<{ age: z.ZodOptional<z.z.ZodNumber>; pet: co.Optional<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>; }>(shape: { age: z.ZodOptional<z.z.ZodNumber>; pet: co.Optional<...>; }): co.Map<...> export mapage: z.ZodOptional<z.z.ZodNumber>age:import zz.optional(optional<z.z.ZodNumber>(innerType: z.z.ZodNumber): z.ZodOptional<z.z.ZodNumber> export optionalimport zz.number()), // primitivefunction number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber export numberpet:pet: co.Optional<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>import coco.optional(optional<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>(schema: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>): co.Optional<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>> export optionalPet), // CoValue });const Pet: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
Recursive References
You can wrap references in getters. This allows you to defer evaluation until the property is accessed. This technique is particularly useful for defining circular references, including recursive (self-referencing) schemas, or mutually recursive schemas.
constPerson =const Person: co.Map<{ name: z.z.ZodString; readonly bestFriend: co.Map<..., unknown, Account | Group>; }, unknown, Account | Group>import coco.map({map<{ name: z.z.ZodString; readonly bestFriend: co.Map<..., unknown, Account | Group>; }>(shape: { name: z.z.ZodString; readonly bestFriend: co.Map<..., unknown, Account | Group>; }): co.Map<...> export mapname: z.z.ZodStringname:import zz.string(), getfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringbestFriend() { returnbestFriend: co.Map<{ name: z.z.ZodString; readonly bestFriend: co.Map<..., unknown, Account | Group>; }, unknown, Account | Group>Person; } });const Person: co.Map<{ name: z.z.ZodString; readonly bestFriend: co.Map<..., unknown, Account | Group>; }, unknown, Account | Group>
You can use the same technique for mutually recursive references:
import {import coco,import zz } from "jazz-tools"; constPerson =const Person: co.Map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }, unknown, Account | Group>import coco.map({map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }>(shape: { name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }): co.Map<...> export mapname: z.z.ZodStringname:import zz.string(), getfunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringfriends() { returnfriends: co.List<co.Map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }, unknown, Account | Group>>ListOfPeople; } }); constconst ListOfPeople: co.List<co.Map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }, unknown, Account | Group>>ListOfPeople =const ListOfPeople: co.List<co.Map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }, unknown, Account | Group>>import coco.list(list<co.Map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }, unknown, Account | Group>>(element: co.Map<{ name: z.z.ZodString; readonly friends: co.List<...>; }, unknown, Account | Group>): co.List<...> export listPerson);const Person: co.Map<{ name: z.z.ZodString; readonly friends: co.List<co.Map<..., unknown, Account | Group>>; }, unknown, Account | Group>
If you try to reference ListOfPeople in Person without using a getter, you'll run into a ReferenceError because of the temporal dead zone.
Helper methods
If you find yourself repeating the same logic to access computed CoValues properties, you can define helper functions to encapsulate it for better reusability:
constPerson =const Person: co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; dateOfBirth: z.z.ZodDate; }, unknown, Account | Group>import coco.map({map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; dateOfBirth: z.z.ZodDate; }>(shape: { firstName: z.z.ZodString; lastName: z.z.ZodString; dateOfBirth: z.z.ZodDate; }): co.Map<...> export mapfirstName: z.z.ZodStringfirstName:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringlastName: z.z.ZodStringlastName:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringdateOfBirth: z.z.ZodDatedateOfBirth:import zz.date(), }); typefunction date(params?: string | z.z.core.$ZodDateParams): z.z.ZodDate export datePerson =type Person = { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapimport coco.loaded<typeoftype loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loadedPerson>; export functionconst Person: co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; dateOfBirth: z.z.ZodDate; }, unknown, Account | Group>function getPersonFullName(person: Person): stringgetPersonFullName(person:person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapPerson) { return `${type Person = { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapperson.person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapfirstName: stringfirstName} ${person.person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMaplastName: stringlastName}`; } export functionfunction getPersonAgeAsOf(person: Person, date: Date): numbergetPersonAgeAsOf(person:person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapPerson,type Person = { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapdate: Datedate: Date) { returnfunction differenceInYears(date1: Date, date2: Date): numberdifferenceInYears(date: Datedate,person.person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapdateOfBirth: DatedateOfBirth); } constperson =const person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapPerson.const Person: co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; dateOfBirth: z.z.ZodDate; }, unknown, Account | Group>create({CoMapSchema<{ firstName: ZodString; lastName: ZodString; dateOfBirth: ZodDate; }, unknown, Account | Group>.create(init: { firstName: string; lastName: string; dateOfBirth: Date; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)firstName: stringfirstName: "John",lastName: stringlastName: "Doe",dateOfBirth: DatedateOfBirth: newDate("1990-01-01"), }); constvar Date: DateConstructor new (value: number | string | Date) => Date (+4 overloads)const fullName: stringfullName =function getPersonFullName(person: Person): stringgetPersonFullName(person); constconst person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapconst age: numberage =function getPersonAgeAsOf(person: Person, date: Date): numbergetPersonAgeAsOf(person, newconst person: { readonly firstName: string; readonly lastName: string; readonly dateOfBirth: Date; } & CoMapDate());var Date: DateConstructor new () => Date (+4 overloads)
Similarly, you can encapsulate logic needed to update CoValues:
constPerson =const Person: co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; }, unknown, Account | Group>import coco.map({map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; }>(shape: { firstName: z.z.ZodString; lastName: z.z.ZodString; }): co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; }, unknown, Account | Group> export mapfirstName: z.z.ZodStringfirstName:import zz.string(),function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringlastName: z.z.ZodStringlastName:import zz.string(), }); typefunction string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export stringPerson =type Person = { readonly firstName: string; readonly lastName: string; } & CoMapimport coco.loaded<typeoftype loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loadedPerson>; export functionconst Person: co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; }, unknown, Account | Group>function updatePersonName(person: Person, fullName: string): voidupdatePersonName(person:person: { readonly firstName: string; readonly lastName: string; } & CoMapPerson,type Person = { readonly firstName: string; readonly lastName: string; } & CoMapfullName: stringfullName: string) { const [const firstName: stringfirstName,const lastName: stringlastName] =fullName: stringfullName.String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)Split a string into substrings using the specified separator and return them as an array.split(" ");person.person: { readonly firstName: string; readonly lastName: string; } & CoMapCoMap.$jazz: CoMapJazzApi<{ readonly firstName: string; readonly lastName: string; } & CoMap>Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<{ readonly firstName: string; readonly lastName: string; } & CoMap>.set<"firstName">(key: "firstName", value: string): voidSet a value on the CoMapset("firstName",const firstName: stringfirstName);person.person: { readonly firstName: string; readonly lastName: string; } & CoMapCoMap.$jazz: CoMapJazzApi<{ readonly firstName: string; readonly lastName: string; } & CoMap>Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<{ readonly firstName: string; readonly lastName: string; } & CoMap>.set<"lastName">(key: "lastName", value: string): voidSet a value on the CoMapset("lastName",const lastName: stringlastName); } constperson =const person: { readonly firstName: string; readonly lastName: string; } & CoMapPerson.const Person: co.Map<{ firstName: z.z.ZodString; lastName: z.z.ZodString; }, unknown, Account | Group>create({CoMapSchema<{ firstName: ZodString; lastName: ZodString; }, unknown, Account | Group>.create(init: { firstName: string; lastName: string; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)firstName: stringfirstName: "John",lastName: stringlastName: "Doe", });var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(person.const person: { readonly firstName: string; readonly lastName: string; } & CoMapfirstName: stringfirstName,person.const person: { readonly firstName: string; readonly lastName: string; } & CoMaplastName: stringlastName) // John Doefunction updatePersonName(person: Person, fullName: string): voidupdatePersonName(person, "Jane Doe");const person: { readonly firstName: string; readonly lastName: string; } & CoMapvar console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(person.const person: { readonly firstName: string; readonly lastName: string; } & CoMapfirstName: stringfirstName,person.const person: { readonly firstName: string; readonly lastName: string; } & CoMaplastName: stringlastName) // Jane Doe