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's load method, or narrow the type first
  • $jazz.subscribe — use the union schema's subscribe method
  • $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.