David Alonso
David Alonso•9mo ago

Typescript: Checking if value is part of a v.union(v.literal()) type

How do I accomplish the following check?
No description
46 Replies
David Alonso
David AlonsoOP•9mo ago
export type PageBlock = Extract<Doc<"blocks">, { type: "page" }>;

export type GridBlock =
| PageBlock
| TableBlock
| DocumentReferenceBlock
...
type GridBlockType = GridBlock["type"];
export type PageBlock = Extract<Doc<"blocks">, { type: "page" }>;

export type GridBlock =
| PageBlock
| TableBlock
| DocumentReferenceBlock
...
type GridBlockType = GridBlock["type"];
Michal Srb
Michal Srb•9mo ago
The types are TS types, they cannot be used to perform runtime checks. You'll need a runtime value, like a set:
const validTypes = new Set(["a", "b", "c"])
if (validTypes.has(block.type)) {
...
const validTypes = new Set(["a", "b", "c"])
if (validTypes.has(block.type)) {
...
David Alonso
David AlonsoOP•9mo ago
hmm i see, I'm trying to find a solution that avoids duplication. Is there a way I can do the as GridBlock cast with better safety? I was initially thinking of putting a validator vGridBlock on the argument of patchBlockPropertiesWithNewGridLayout but I don't think that works out of the box. I apologize if this doesn't make sense, I'm quite inexperienced with validators and robust type checking, so very open to get feedback on how you'd implement this. I'm also checking https://stack.convex.dev/typescript-zod-function-validation
const patchBlockPropertiesWithNewGridLayout = async (
context: MutationCtx,
block: GridBlock,
gridItemLayout: GridItemLayoutProps
) => {
return await context.db.patch(block._id, {
properties: { ...block.properties, gridLayout: gridItemLayout },
});
};

export const updateBlockGridLayoutProps = mutation({
args: { blockId: v.id("blocks"), gridItemLayout: vGridItemLayoutProps },
handler: async (context, args) => {
const block = await context.db.get(args.blockId);
if (!block) throw new Error("Block not found");
return await patchBlockPropertiesWithNewGridLayout(
context,
block as GridBlock,
args.gridItemLayout
);
},
});
const patchBlockPropertiesWithNewGridLayout = async (
context: MutationCtx,
block: GridBlock,
gridItemLayout: GridItemLayoutProps
) => {
return await context.db.patch(block._id, {
properties: { ...block.properties, gridLayout: gridItemLayout },
});
};

export const updateBlockGridLayoutProps = mutation({
args: { blockId: v.id("blocks"), gridItemLayout: vGridItemLayoutProps },
handler: async (context, args) => {
const block = await context.db.get(args.blockId);
if (!block) throw new Error("Block not found");
return await patchBlockPropertiesWithNewGridLayout(
context,
block as GridBlock,
args.gridItemLayout
);
},
});
Zod with TypeScript for Server-side Validation and End-to-End Types
Use Zod with TypeScript for argument validation on your server functions allows you to both protect against invalid data, and define TypeScript types ...
David Alonso
David AlonsoOP•9mo ago
Hi @Michal Srb maybe somewhat related to this in the sense that I'm trying to do type reuse to avoid things going out of sync Suppose I have the following:
export const vUnionValidator = v.union(
v.object({
type: v.literal("string"),
}),
v.object({
type: v.literal("int"),
}),
);
export const vUnionValidator = v.union(
v.object({
type: v.literal("string"),
}),
v.object({
type: v.literal("int"),
}),
);
Am I able to extract a validator object that is the same as v.union(v.literal("string"),v.literal("int"))? I want to use validator as the type of an argument to a mutation for instance, let me know if there's an easier way 🙂 Also would be curious in what cases you'd make use of an auxiliary enum, e.g.
export enum TypeEnum {
string = "string",
int = "int",
}

export const vUnionValidator = v.union(
v.object({
type: v.literal(FirestoreFieldType.string),
}),
v.object({
type: v.literal(FirestoreFieldType.int),
}),
);
export enum TypeEnum {
string = "string",
int = "int",
}

export const vUnionValidator = v.union(
v.object({
type: v.literal(FirestoreFieldType.string),
}),
v.object({
type: v.literal(FirestoreFieldType.int),
}),
);
and if so, if there's an easy way to go from an enum definition to a validator that's a union of literals? @sshader if you're online I'd appreciate your input on this, we make heavy use of literals everywhere and we're getting stuck in a few places whenever we have to use validators and would really like to understand the recommended approach
David Alonso
David AlonsoOP•9mo ago
npm
convex-helpers
A collection of useful code to complement the official convex package.. Latest version: 0.1.41, last published: 17 days ago. Start using convex-helpers in your project by running npm i convex-helpers. There are no other projects in the npm registry using convex-helpers.
David Alonso
David AlonsoOP•9mo ago
Going down the rabbithole... trying to use Table for my existing tables:
const vDataSource = v.union(
v.object({
...commonDataSourceProps,
type: v.literal("firestore"),
}),
v.object({
...commonDataSourceProps,
type: v.literal("bigquery"),
})
);
export const DataSource = Table("dataSources", vDataSource);
const vDataSource = v.union(
v.object({
...commonDataSourceProps,
type: v.literal("firestore"),
}),
v.object({
...commonDataSourceProps,
type: v.literal("bigquery"),
})
);
export const DataSource = Table("dataSources", vDataSource);
I cannot pass in a validator and I'm not sure how to represent this union in the expected format Maybe I can use this pattern inside convex function args?
literalUnionParam: v.string() as Validator<Doc<"dataSources">["type"]>,
literalUnionParam: v.string() as Validator<Doc<"dataSources">["type"]>,
but I guess this is not the same as:
pick(DataSource.withoutSystemFields, ["type"]);
pick(DataSource.withoutSystemFields, ["type"]);
? @ian If I try to use this pattern inside a validator object (e.g. for a table schema definition) I get a weird warning saying that Doc is not generic... apparently I can do this instead:
export const LiteralUnionType = {
string = "string",
int = "int"
} as const;
const vLiteralUnionType = v.string() as Validator<typeof LiteralUnionType[keyof typeof LiteralUnionType]>;
export const LiteralUnionType = {
string = "string",
int = "int"
} as const;
const vLiteralUnionType = v.string() as Validator<typeof LiteralUnionType[keyof typeof LiteralUnionType]>;
ian
ian•9mo ago
Hey @David Alonso - very glad to be hearing these questions, since we're actively improving this and I'm glad to get the validation that it's useful! I think you've discovered a lot already. I really like the type hacking to reach into the discriminated union - that seems like a good way to get the type-time validation. The runtime (and type time!) validation will be easier with the changes we're making, which allows you to introspect validators and maintain types. So you could do vDataSource.members[0].fields.type.value and the runtime value and typescript type would be "firestore". So you can extract the values for validation, along with the types - though I'm not sure if simply doing vDataSource.members.map(m => m.fields.type.value) would give you the good type union, I hope it would! cc @ballingt who is working on getting this out soon (~days or ~weeks)
David Alonso
David AlonsoOP•9mo ago
Thanks for the input @ian , I look forward to these changes! do you know how I can get around this? (it's a bit unrelated)
ian
ian•9mo ago
What do you want .withoutSystemFields to return? Just getting the type out? If the union truly only differs with type I wonder if you could do something like:
const types = [ "firestore", "bigquery" ] as const;
const dataSourceFields = {
...commonDataSourceProps,
type: literals(...types),
};
const DataSource = Table("dataSources", dataSourceFields);
const types = [ "firestore", "bigquery" ] as const;
const dataSourceFields = {
...commonDataSourceProps,
type: literals(...types),
};
const DataSource = Table("dataSources", dataSourceFields);
David Alonso
David AlonsoOP•9mo ago
sorry my example wasn't good, here's a more representative one:
const vDataSource = v.union(
v.object({
firestoreId: v.string(),
type: v.literal("firestore"),
}),
v.object({
bqTable: v.id("fdsfds"),
type: v.literal("bigquery"),
})
);
export const DataSource = Table("dataSources", vDataSource);
const vDataSource = v.union(
v.object({
firestoreId: v.string(),
type: v.literal("firestore"),
}),
v.object({
bqTable: v.id("fdsfds"),
type: v.literal("bigquery"),
})
);
export const DataSource = Table("dataSources", vDataSource);
this is a pattern we use a TON in our schema ~50% of our tables
ian
ian•9mo ago
gotcha - yeah I often try to hide the extra data in a sub-field, like
{
data: v.union(
v.object({
kind: ...
{
data: v.union(
v.object({
kind: ...
So tactically (and hoping the new way ships in a few days), I wonder if this would work:
const dataSourceFields = [
{
firestoreId: v.string(),
type: "firestore" as const,
},
{
bqTable: v.id("fdsfds"),
type: "bigquery" as const,
}
] as const;
const types: Array<dataSourceFields[number].type> = dataSourceFields.map(f => f.type);
const vTypes = literals(...types);
const dataSourceObjects: { [K in keyof typeof dataSourceFields]: ObjectValidator<ObjectType<Omit<dataSourceFields[K], "type">> & { type: dataSourceFields[K]["type"] }> }
= dataSourceFields.map(ds => v.object({ ...ds, type: v.literal(ds.type) }));
const vDataSourceFields = v.union(dataSourceObjects[0], dataSourceObjects[1], ...dataSourceObjects.slice(2))
const dataSourceFields = [
{
firestoreId: v.string(),
type: "firestore" as const,
},
{
bqTable: v.id("fdsfds"),
type: "bigquery" as const,
}
] as const;
const types: Array<dataSourceFields[number].type> = dataSourceFields.map(f => f.type);
const vTypes = literals(...types);
const dataSourceObjects: { [K in keyof typeof dataSourceFields]: ObjectValidator<ObjectType<Omit<dataSourceFields[K], "type">> & { type: dataSourceFields[K]["type"] }> }
= dataSourceFields.map(ds => v.object({ ...ds, type: v.literal(ds.type) }));
const vDataSourceFields = v.union(dataSourceObjects[0], dataSourceObjects[1], ...dataSourceObjects.slice(2))
That's all I have in me tonight - good luck!
David Alonso
David AlonsoOP•9mo ago
Thanks so much! Please keep me posted on when you ship new stuff! I wasn't able to get the above to work (maybe due to incorrect imports). Is dataSourceObjects what you'd pass into Table()?
ian
ian•9mo ago
In this case I would not use Table. Table is a handy utility for keeping references to the fields / validator for the doc / etc, but it isn't quite as obvious what it should do for unions for e.g. withoutSystemFields - should it be an array? Where are you hoping to use it like Table? Each use case hopefully can just compose these things
David Alonso
David AlonsoOP•9mo ago
The reason I wanted to use a Table everywhere is because of access to these utils, which seem super useful:
// A validator just for balance & email: { balance: v.union(...), email: ..}
const balanceAndEmail = pick(Account.withoutSystemFields, ["balance", "email"]);

// A validator for all the fields except balance.
const accountWithoutBalance = omit(Account.withSystemFields, ["balance"]);
// A validator just for balance & email: { balance: v.union(...), email: ..}
const balanceAndEmail = pick(Account.withoutSystemFields, ["balance", "email"]);

// A validator for all the fields except balance.
const accountWithoutBalance = omit(Account.withSystemFields, ["balance"]);
ian
ian•9mo ago
In the next release, this syntax works (and note no helpers needed!):
const vDataSource = v.union(
v.object( {
firestoreId: v.string(),
type: v.literal("firestore"),
} ),
v.object({
bqTable: v.id("fdsfds"),
type: v.literal("bigquery"),
}),
);
const dataSourceTypes = vDataSource.members.map((f) => f.fields.type.value); // type: ("firestore" | "bigquery")[]
const vDataSourceTypes = v.union(...dataSourceTypes.map(v.literal));
const vDataSource = v.union(
v.object( {
firestoreId: v.string(),
type: v.literal("firestore"),
} ),
v.object({
bqTable: v.id("fdsfds"),
type: v.literal("bigquery"),
}),
);
const dataSourceTypes = vDataSource.members.map((f) => f.fields.type.value); // type: ("firestore" | "bigquery")[]
const vDataSourceTypes = v.union(...dataSourceTypes.map(v.literal));
and you can do schema.tables.dataSources.validator.fields to get withoutSystemFields 🎉
David Alonso
David AlonsoOP•9mo ago
that's great! two questions: 1. when is this next release? 2. what would be the equivalent of pick and omit helpers in this new release to get partial validators?
ian
ian•9mo ago
1. As soon as tomorrow 2. You could still pick & omit on the .fields of an object validator
David Alonso
David AlonsoOP•9mo ago
Hey @ian , I'm on 1.12.2 bu this doesn't work yet:
const dataSourceTypes = vDataSource.members.map((f) => f.fields.type.value); // type: ("firestore" | "bigquery")[]
const dataSourceTypes = vDataSource.members.map((f) => f.fields.type.value); // type: ("firestore" | "bigquery")[]
is this expected?
Michal Srb
Michal Srb•9mo ago
We haven't made the release yet
David Alonso
David AlonsoOP•8mo ago
is it in 1.13?
Michal Srb
Michal Srb•8mo ago
Yup
David Alonso
David AlonsoOP•8mo ago
Hey @ian is there a blog post or something coming with some more usage examples? Would be really helpful to start applying this in our codebase
ballingt
ballingt•8mo ago
@David Alonso not immediately planned, any questions about it? try something like schema.tables.users.validator and tab complete your way to success 🙂 There's no pick or omit yet. but you do this manually by spreading (for omit) and choosing (for pick) from validator.fields. @David Alonso feel free to write questions here even if you can figure them out, it'll be helpful for when we write more docs about this.
David Alonso
David AlonsoOP•8mo ago
okay, my immediate questions were about pick and omit can you give a brief working example of spreading and choosing? maybe helpful for others as well
ballingt
ballingt•8mo ago
Here's adding a field and omitting a field: TypeScript Playground
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
ballingt
ballingt•8mo ago
import {v} from "convex/values";

const message = v.object({
body: v.string(),
author: v.string(),
level: v.number(),
});

const addingAField = v.object({
...message.fields,
image: v.string(),
})

const { author, ...rest } = message.fields;
const omittingAField = v.object(rest);

const { level, body } = message.fields;
const choosingFields = v.object({level, body})
import {v} from "convex/values";

const message = v.object({
body: v.string(),
author: v.string(),
level: v.number(),
});

const addingAField = v.object({
...message.fields,
image: v.string(),
})

const { author, ...rest } = message.fields;
const omittingAField = v.object(rest);

const { level, body } = message.fields;
const choosingFields = v.object({level, body})
@David Alonso is this what you're looking for, or do you need functions that do this? I'd love to hear what you're thinking You also don't need to do this fancy spreading, just
const anotherValidator = v.object({
body: message.body,
author: message.author,
});
const anotherValidator = v.object({
body: message.body,
author: message.author,
});
works too.
David Alonso
David AlonsoOP•8mo ago
damn that's so cool, this example really crystalizes it! for now this is what we're planning to use it for, but will let you know if any other use cases pop up
ballingt
ballingt•8mo ago
Here's one of your earlier examples, getting the members of a union
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
David Alonso
David AlonsoOP•8mo ago
How do I handle such a case with Validators?
export const vFilter = v.union(vWhereFilter, vCompositeFilter);

export const vCompositeFilter = v.object({
type: v.union(
v.literal(FilterExpressionType.and),
v.literal(FilterExpressionType.or)
),
filters: v.array(vFilter),
});
export const vFilter = v.union(vWhereFilter, vCompositeFilter);

export const vCompositeFilter = v.object({
type: v.union(
v.literal(FilterExpressionType.and),
v.literal(FilterExpressionType.or)
),
filters: v.array(vFilter),
});
With regular types it works, but in this case I get Block-scoped variable 'vCompositeFilter' used before its declaration.ts(2448)
Michal Srb
Michal Srb•8mo ago
I think you cannot declare recursive types with Convex validators atm.
David Alonso
David AlonsoOP•8mo ago
Previously I've used this kind of pattern as a way to select individual entries in a union
export const FireviewFilterExpressionOperator = {
eq: "==",
notEq: "!=",
lt: "<",
gt: ">",
lte: "<=",
gte: ">=",
arrayContains: "array-contains",
arrayContainsAny: "array-contains-any",
in: "in",
notIn: "not-in",
exists: "exists",
} as const;

export type FireviewFilterExpressionOperator =
(typeof FireviewFilterExpressionOperator)[keyof typeof FireviewFilterExpressionOperator];

export const vFireviewFilterExpressionOperator =
v.string() as Validator<FireviewFilterExpressionOperator>;
export const FireviewFilterExpressionOperator = {
eq: "==",
notEq: "!=",
lt: "<",
gt: ">",
lte: "<=",
gte: ">=",
arrayContains: "array-contains",
arrayContainsAny: "array-contains-any",
in: "in",
notIn: "not-in",
exists: "exists",
} as const;

export type FireviewFilterExpressionOperator =
(typeof FireviewFilterExpressionOperator)[keyof typeof FireviewFilterExpressionOperator];

export const vFireviewFilterExpressionOperator =
v.string() as Validator<FireviewFilterExpressionOperator>;
by doing e.g. FireviewFilterExpressionOperator.notIn or v.literal(FireviewFilterExpressionOperator.notIn) The only way I've found to do this with validators is vFirestoreFilterExpressionOperator.members[0] - is this the recommended approach? It's a little error prone it seems, since it depends on the order of elements in the literal union... maybe a better way to phrase my question is: how do you recommend doing the pick and omit operations that @ballingt shared but over elements of a union - e.g. a union of literals like:
const vFirestoreFilterExpressionOperator = v.union(
v.literal("=="),
v.literal("!="),
v.literal("<"),
v.literal(">"),
v.literal("<="),
v.literal(">="),
v.literal("array-contains"),
v.literal("array-contains-any"),
v.literal("in"),
v.literal("not-in")
);
const vFirestoreFilterExpressionOperator = v.union(
v.literal("=="),
v.literal("!="),
v.literal("<"),
v.literal(">"),
v.literal("<="),
v.literal(">="),
v.literal("array-contains"),
v.literal("array-contains-any"),
v.literal("in"),
v.literal("not-in")
);
any plans to support this? we'd ideally love to have all our types defined as validators and then just use Infer<typeof v...> when needed. This is especially the case now that return validators can be passed to functions
ballingt
ballingt•8mo ago
I'm not sure what you're looking for yet, I assume this isn't it?
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
ballingt
ballingt•8mo ago
It would help to see a code sample of what you'd like — could be that I'm missing it or could be it's not possible but we can recommend something else. No concrete plans to support recursive types, you may not be able to get full database validation if you need this. I'd write your own validation instead, and write wrappers to cast the data to your recursive type when you need to work with it; similar to converting a number to a Date object after you get it out of the database.
David Alonso
David AlonsoOP•8mo ago
that's definitely better but it has limitations for selecting items in the middle efficiently and being immune to changes in the order of the literal union
ballingt
ballingt•8mo ago
once you've put them in the array it'll be hard to do as much with them, so I'd start with the subarrays you need and build up by concating
David Alonso
David AlonsoOP•8mo ago
I'm trying to do this:
returns: fireviewSchema.tables.tableViews.validator.members.filter(
(m) =>
m.fields.layoutType === v.literal("table")
)[0].fields.properties.fields.validator,
returns: fireviewSchema.tables.tableViews.validator.members.filter(
(m) =>
m.fields.layoutType === v.literal("table")
)[0].fields.properties.fields.validator,
but it doesn't seem like the check for the layoutType narrows down the specific member of tableViews that contains the nested type I'm looking for, so I find myself having to do [0] and relying on not changing the order of things at any point another small thing: how do I get the validator with system fields in here?
fireviewSchema.tables.tableViews.validator
fireviewSchema.tables.tableViews.validator
seems to work as a returns validator on queries that return the full document which is a little confusing to me ^ lmk if this would be the recommended way to check if a query returns a type of document or if there's an easier way - but v.doc() doesn't exist afaik this is a concrete example of what I'm doing atm:
const { fieldId, ...fieldsLayoutExcludingFieldId } =
fireviewSchema.tables.tableViews.validator.members[0].fields.properties.fields
.fieldsLayout.element.fields;

export const getFieldsLayoutByTableViewId = query({
args: {
viewId: v.id("tableViews"),
},
returns: v.array(
v.object({
...fieldsLayoutExcludingFieldId,
field: fireviewSchema.tables.firestoreFields.validator,
})
),
const { fieldId, ...fieldsLayoutExcludingFieldId } =
fireviewSchema.tables.tableViews.validator.members[0].fields.properties.fields
.fieldsLayout.element.fields;

export const getFieldsLayoutByTableViewId = query({
args: {
viewId: v.id("tableViews"),
},
returns: v.array(
v.object({
...fieldsLayoutExcludingFieldId,
field: fireviewSchema.tables.firestoreFields.validator,
})
),
So if I need to pass an object of this type into a mutation should I just set the validator to v.any()?
export type LocalFilterExpression =
| {
type: "and" | "or";
filters: LocalFilterExpression[];
}
| {
type: "where";
fieldId: Id<"firestoreFields">;
operator: FireviewFilterExpressionOperator;
value: any;
};
export type LocalFilterExpression =
| {
type: "and" | "or";
filters: LocalFilterExpression[];
}
| {
type: "where";
fieldId: Id<"firestoreFields">;
operator: FireviewFilterExpressionOperator;
value: any;
};
ian
ian•8mo ago
To add system fields, you could spread in the fields, or use a convex-helpers validator utility like systemFields("firestoreFields") or get all of the fields using withSystemFields("firestoreFields", fireviewSchema.tables.firestoreFields.validator.fields). There's a utility I've started writing that would work like doc(schema, "firestoreFields") but there's some limitations around it returning a generic validator type instead of specifically a VUnion / VObject based on what kind you had in your schema. but happy to share the in-progress idea if you want to play with it @sshader has also played with making a more powerful v , let's call it vv, that you could make like const vv = betterV(schema) and do things like vv.doc("firestoreFields") and a type-safe vv.id("onlyValidTableNamesHere")
David Alonso
David AlonsoOP•8mo ago
This: withSystemFields("firestoreFields", fireviewSchema.tables.firestoreFields.validator.fields) doesn't work for this type since it's a union. I tried appending `.map((m) => m.fields) but that also didn't work for some reason, I'll stick with spreading for now but definitely keep me posted! Recursive validators would especially be super helpful to have full coverage of our types This is the closest I can get:
field: v.union(
...fireviewSchema.tables.firestoreFields.validator.members.map((m) => withSystemFields("firestoreFields", m.fields))
),
field: v.union(
...fireviewSchema.tables.firestoreFields.validator.members.map((m) => withSystemFields("firestoreFields", m.fields))
),
but this doesn't work sadly
ian
ian•8mo ago
Try this for now:
export const doc = <
Schema extends SchemaDefinition<any, boolean>,
TableName extends TableNamesInDataModel<
DataModelFromSchemaDefinition<Schema>
>,
>(
schema: Schema,
tableName: TableName,
): Validator<
DocumentByName<DataModelFromSchemaDefinition<Schema>, TableName>,
"required",
FieldPaths<NamedTableInfo<DataModelFromSchemaDefinition<Schema>, TableName>>
> => {
const docValidator = (validator: Validator<any, any, any>) => {
if (validator.kind === "object") {
return v.object({
...validator.fields,
...systemFields(tableName),
});
}
if (validator.kind !== "union") {
throw new Error(
"Only object and union validators are supported for documents",
);
}
return v.union(...validator.members.map(docValidator));
}
return docValidator(schema.tables[tableName].validator);
};
export const doc = <
Schema extends SchemaDefinition<any, boolean>,
TableName extends TableNamesInDataModel<
DataModelFromSchemaDefinition<Schema>
>,
>(
schema: Schema,
tableName: TableName,
): Validator<
DocumentByName<DataModelFromSchemaDefinition<Schema>, TableName>,
"required",
FieldPaths<NamedTableInfo<DataModelFromSchemaDefinition<Schema>, TableName>>
> => {
const docValidator = (validator: Validator<any, any, any>) => {
if (validator.kind === "object") {
return v.object({
...validator.fields,
...systemFields(tableName),
});
}
if (validator.kind !== "union") {
throw new Error(
"Only object and union validators are supported for documents",
);
}
return v.union(...validator.members.map(docValidator));
}
return docValidator(schema.tables[tableName].validator);
};
like doc(schema, 'firestoreFields') where schema is imported from "./schema" it doesn't give the full .members introspection but the document typescript types and runtime validators should be right I haven't tested it, and just caught a bug but hopefully it's a good start until I can circle back here with something more robust. Also lmk if you end up with something worth sharing!
David Alonso
David AlonsoOP•8mo ago
this is already super useful! Thanks 🫶 I do see these errors in the console:
convex/helpers/documentValidator.ts(30,23): error TS2345: Argument of type '{ _id: Validator<Id<TableName>, false, never>; _creationTime: Validator<number, false, never>; }' is not assignable to parameter of type 'PropertyValidators'.
[TSC] Property '_id' is incompatible with index signature.
[TSC] Type 'Validator<Id<TableName>, false, never>' is not assignable to type 'Validator<any, OptionalProperty, any>'.
[TSC] Type 'VId<Id<TableName>, false>' is not assignable to type 'Validator<any, OptionalProperty, any>'.
[TSC] Type 'VId<Id<TableName>, false>' is not assignable to type 'VId<any, OptionalProperty>'.
[TSC] Type 'false' is not assignable to type 'OptionalProperty'.
convex/helpers/documentValidator.ts(30,23): error TS2345: Argument of type '{ _id: Validator<Id<TableName>, false, never>; _creationTime: Validator<number, false, never>; }' is not assignable to parameter of type 'PropertyValidators'.
[TSC] Property '_id' is incompatible with index signature.
[TSC] Type 'Validator<Id<TableName>, false, never>' is not assignable to type 'Validator<any, OptionalProperty, any>'.
[TSC] Type 'VId<Id<TableName>, false>' is not assignable to type 'Validator<any, OptionalProperty, any>'.
[TSC] Type 'VId<Id<TableName>, false>' is not assignable to type 'VId<any, OptionalProperty>'.
[TSC] Type 'false' is not assignable to type 'OptionalProperty'.
ian
ian•8mo ago
It's surprising that the type has "false" for Validator's second param - it should be "required" or "optional" - is that from some code you have somewhere? I wonder if you're using an older version of convex-helpers that's using the older convex package that had it as a boolean
David Alonso
David AlonsoOP•7mo ago
ah it's because of an old version of the helpers, resolved! Question: can I create recursive validators by creating a recursive zod type and then using zodToConvex?
Michal Srb
Michal Srb•7mo ago
I don't think so (in the sense that it won't do the runtime validation recursively, that's not supported - it might pretend to give you a recursive type though)
David Alonso
David AlonsoOP•7mo ago
hmm okay so if I want the recursive runtime validation I should probably use zCustomQuery or sth like that? okay, when I do this:
export type LocalFilterExpression =
| {
type: Infer<typeof vJoinFilterOperatorType>;
filters: LocalFilterExpression[];
}
| z.infer<typeof zFireviewWhereFilter>;

export const zLocalFilterExpression: z.ZodType<LocalFilterExpression> = z.lazy(
() =>
z.union([
z.object({
type: z.union([z.literal("and"), z.literal("or")]),
filters: z.array(zLocalFilterExpression),
}),
zFireviewWhereFilter,
])
);

export const vLocalFilterExpression = zodToConvex(zLocalFilterExpression);
export type LocalFilterExpression =
| {
type: Infer<typeof vJoinFilterOperatorType>;
filters: LocalFilterExpression[];
}
| z.infer<typeof zFireviewWhereFilter>;

export const zLocalFilterExpression: z.ZodType<LocalFilterExpression> = z.lazy(
() =>
z.union([
z.object({
type: z.union([z.literal("and"), z.literal("or")]),
filters: z.array(zLocalFilterExpression),
}),
zFireviewWhereFilter,
])
);

export const vLocalFilterExpression = zodToConvex(zLocalFilterExpression);
vLocalFilterExpression is never so I guess I'll have to try with zCustomQuery Okay zCustomQuery also doesn't work...
Error fetching POST https://<>.convex.cloud/api/push_config 400 Bad Request: InvalidModules: Hit an error while pushing:
Loading the pushed modules encountered the following
error:
Failed to analyze mutations/filterExpressions/auth.js: Uncaught RangeError: Maximum call stack size exceeded
at map [as map] (<anonymous>)
at zodToConvex (../../../../node_modules/.pnpm/convex-helpers@0.1.49-alpha.2_convex@1.13.0_@clerk+clerk-react@5.2.7_react-dom@18.3.1_react@1_kgus2m7floqasu6iwwsemsc5wa/node_modules/convex-helpers/dist/server/zod.js:345:13)
Error fetching POST https://<>.convex.cloud/api/push_config 400 Bad Request: InvalidModules: Hit an error while pushing:
Loading the pushed modules encountered the following
error:
Failed to analyze mutations/filterExpressions/auth.js: Uncaught RangeError: Maximum call stack size exceeded
at map [as map] (<anonymous>)
at zodToConvex (../../../../node_modules/.pnpm/convex-helpers@0.1.49-alpha.2_convex@1.13.0_@clerk+clerk-react@5.2.7_react-dom@18.3.1_react@1_kgus2m7floqasu6iwwsemsc5wa/node_modules/convex-helpers/dist/server/zod.js:345:13)
How hard is it to support this? Seems like Zod supports doing zRecursiveType.parse() so I can do it manually for now I guess?
Michal Srb
Michal Srb•7mo ago
Yeah, you can definitely validate in JS. One day we might add support for it in the Convex backend. We just need an equivalent to z.lazy that'll work across the boundary in Rust.
David Alonso
David AlonsoOP•7mo ago
That would be great, I'm sure this won't be the last recursive type we use for function interfaces / schema definition

Did you find this page helpful?