wantpinow
wantpinow6h ago

Intersection of Discriminated Unions

First of all, I love Convex - great job team ❤️ I have a use case where I need to create a convex schema that is an intersection of two discriminated unions. I have the following toy example that works with Zod but I can't see an easy way of implementing the same behavior using Convex. In the example below we start with two unions: 1. People vs animals 2. Countries vs cities Ideally, in convex, I would be able to easily define the intersection of those two, allowing me to store objects like this:
{
entityType: "animal",
species: "dog",
locationType: "city",
locationName: "New York",
}
{
entityType: "animal",
species: "dog",
locationType: "city",
locationName: "New York",
}
I know I can write a function to get all NxM possible convex objects, but I was just wondering if there is cleaner approach, similar to what I can do with Zod
zodExample.ts
import { z } from "zod";

// person vs animal discriminated union
const person = z.object({
entityType: z.literal("person"),
personName: z.string(),
age: z.number(),
});

const animal = z.object({
entityType: z.literal("animal"),
species: z.string(),
});

const personOrAnimal = z.union([person, animal]);

const examplePersonOrAnimal: z.infer<typeof personOrAnimal> = {
entityType: "animal",
species: "dog",
};

// location discriminated union
const country = z.object({
locationType: z.literal("country"),
locationName: z.string(),
});

const city = z.object({
locationType: z.literal("city"),
locationName: z.string(),
});

const location = z.union([country, city]);

const exampleLocation: z.infer<typeof location> = {
locationType: "city",
locationName: "New York",
};

// combined NxN union
const entityAtLocation = z.intersection(personOrAnimal, location);

const exampleEntityAtLocation: z.infer<typeof entityAtLocation> = {
entityType: "animal",
species: "dog",
locationType: "city",
locationName: "New York",
};
zodExample.ts
import { z } from "zod";

// person vs animal discriminated union
const person = z.object({
entityType: z.literal("person"),
personName: z.string(),
age: z.number(),
});

const animal = z.object({
entityType: z.literal("animal"),
species: z.string(),
});

const personOrAnimal = z.union([person, animal]);

const examplePersonOrAnimal: z.infer<typeof personOrAnimal> = {
entityType: "animal",
species: "dog",
};

// location discriminated union
const country = z.object({
locationType: z.literal("country"),
locationName: z.string(),
});

const city = z.object({
locationType: z.literal("city"),
locationName: z.string(),
});

const location = z.union([country, city]);

const exampleLocation: z.infer<typeof location> = {
locationType: "city",
locationName: "New York",
};

// combined NxN union
const entityAtLocation = z.intersection(personOrAnimal, location);

const exampleEntityAtLocation: z.infer<typeof entityAtLocation> = {
entityType: "animal",
species: "dog",
locationType: "city",
locationName: "New York",
};
4 Replies
Convex Bot
Convex Bot6h ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
RJ
RJ4h ago
I'm not aware of a cleaner approach than writing a function that does this yourself. Generic support could probably be added to the zodToConvex helper in convex-helpers, so that you could just use the above Zod validator directly, if you were interested in contributing. Other options that don't accomplish exactly what you're asking for include not flattening these but keeping everything in the same table still:
const vEntityAtLocation = v.object({
entity: vEntity,
location: vLocation,
})
const vEntityAtLocation = v.object({
entity: vEntity,
location: vLocation,
})
Or having separate entity and location tables:
const vEntityAtLocation = v.object({
entity: v.id("entities"),
location: v.id("locations"),
})
const vEntityAtLocation = v.object({
entity: v.id("entities"),
location: v.id("locations"),
})
wantpinow
wantpinowOP2h ago
Thanks for the reply @RJ, I assumed that might be the case. Unfortunately I have to keep them flat as i'm modelling the schema off of an external API, but the function approach will work okay for now. I'll take a look at zodToConvex and make a PR if I come up with a workable solution
RJ
RJ2h ago
No problem! And makes sense

Did you find this page helpful?