Schema Unions
Schema unions allow you to create types that can be one of several different schemas, similar to TypeScript union types. They use a discriminator field to determine which specific schema an instance represents at runtime, enabling type-safe polymorphism in your Jazz applications.
The following operations are not available in schema unions:
$jazz.ensureLoaded— use the union schema'sloadmethod, or narrow the type first$jazz.subscribe— use the union schema'ssubscribemethod$jazz.set— use$jazz.applyDiff
Creating schema unions
Schema unions are defined with co.discriminatedUnion() by providing an array of schemas and a discriminator field.
The discriminator field must be a z.literal().
export const ButtonWidget = co.map({ type: z.literal("button"), label: z.string(), }); export const SliderWidget = co.map({ type: z.literal("slider"), min: z.number(), max: z.number(), }); export const WidgetUnion = co.discriminatedUnion("type", [ ButtonWidget, SliderWidget, ]);
To instantiate a schema union, just use the create method of one of the member schemas:
const dashboard = Dashboard.create({ widgets: [ ButtonWidget.create({ type: "button", label: "Click me" }), SliderWidget.create({ type: "slider", min: 0, max: 100 }), ], });
You can also use plain JSON objects, and let Jazz infer the concrete type from the discriminator field:
const dashboardFromJSON = Dashboard.create({ widgets: [ { type: "button", label: "Click me" }, { type: "slider", min: 0, max: 100 }, ], });
Narrowing unions
When working with schema unions, you can access any property that is common to all members of the union. To access properties specific to a particular union member, you need to narrow the type. You can do this using a TypeScript type guard on the discriminator field:
dashboard.widgets.forEach((widget) => { if (widget.type === "button") { console.log(`Button: ${widget.label}`); } else if (widget.type === "slider") { console.log(`Slider: ${widget.min} to ${widget.max}`); } });
Loading schema unions
You can load an instance of a schema union using its ID, without having to know its concrete type:
const widget = await WidgetUnion.load(widgetId); // Subscribe to updates const unsubscribe = WidgetUnion.subscribe(widgetId, {}, (widget) => { console.log("Widget updated:", widget); });
Nested schema unions
You can create complex hierarchies by nesting discriminated unions within other unions:
// Define error types const BadRequestError = co.map({ status: z.literal("failed"), code: z.literal(400), message: z.string(), }); const UnauthorizedError = co.map({ status: z.literal("failed"), code: z.literal(401), message: z.string(), }); const InternalServerError = co.map({ status: z.literal("failed"), code: z.literal(500), message: z.string(), }); // Create a union of error types const ErrorResponse = co.discriminatedUnion("code", [ BadRequestError, UnauthorizedError, InternalServerError, ]); // Define success type const SuccessResponse = co.map({ status: z.literal("success"), data: z.string(), }); // Create a top-level union that includes the error union const ApiResponse = co.discriminatedUnion("status", [ SuccessResponse, ErrorResponse, ]); function handleResponse(response: co.loaded<typeof ApiResponse>) { if (response.status === "success") { console.log("Success:", response.data); } else { // This is an error - narrow further by error code if (response.code === 400) { console.log("Bad request:", response.message); } else if (response.code === 401) { console.log("Unauthorized:", response.message); } else if (response.code === 500) { console.log("Server error:", response.message); } } }
Limitations with schema unions
Schema unions have some limitations that you should be aware of. They are due to TypeScript behaviour with type unions: when the type members of the union have methods with generic parameters, TypeScript will not allow calling those methods on the union type. This affects some of the methods on the $jazz namespace.
Note that these methods may still work at runtime, but their use is not recommended as you will lose type safety.
$jazz.ensureLoaded and $jazz.subscribe require type narrowing
The $jazz.ensureLoaded and $jazz.subscribe methods are not supported directly on a schema union unless you first narrow the type using the discriminator.
Updating union fields
You can't use $jazz.set to modify a schema union's fields (even if the field is present in all the union members).
Use $jazz.applyDiff instead.