Bright ideas and techniques for building with Convex.
Profile image
Ian Macartney
6 months ago

Using branded types in validators

Using branded types in validators with type casting in your schema

If you have a more specific type than what you can express with Convex validators, you can still document that at the type level in Convex by casting once in your schema definition.

If you have a type that you use to distinguish different strings, for instance, you might want to make sure you're passing just those types around. E.g. you might have a type:

type MyStringType = string & { __myStringType: never };

brandedString helper

If you just want to get to the code, you can use the convex-helpers helper:

import { brandedString } from "convex-helpers/validators";
import { Infer } from "convex/values";

export const emailValidator = brandedString("email");
export type Email = Infer<typeof emailValidator>;

Read on to learn more about casting in different scenarios.

Casting schema validators

If you want to use this type for Convex, you can only set a field validator as v.string(). However, if you cast it in your schema definition, you'll get the types everywhere automatically:

import { defineSchema, defineTable } from "convex/server";
import { Validator, v } from "convex/values";

defineSchema({
  myTable: defineTable({
    myField: v.string() as Validator<MyStringType>
  })
)}

field will be typed as MyStringType. When you have a query like:

const doc = ctx.db
  .query("myTable")
	.filter(q => q.eq(q.field("myField"), foo)
	.first();

You will have type hints that the parameter for foo needs to be of type MyStringType and you'll get type errors if your type doesn't match.

You'll also see that doc.myField has the type MyStringType when you retrieve it.

Casting argument validators

The same logic applies to function arguments:

export const foo = query({
  args: { bar: v.string() as Validator<MyStringType>},
  handler: async (ctx, args) => {
    //... args.bar is type MyStringType
  },
});

On the client, you'll get type errors if you don't pass MyStringType as the bar parameter.

Can I get into trouble?

Yes! Whenever you use as in TypeScript, you're saying "hey compiler, I know better than you here, so just trust me, k?". Casting to a branded string is less risky than as any. Thankfully, TypeScript will try to save you from glaring errors. For instance, if you do:

    counter: v.number() as Validator<string>,

you could get into a situation where the field expects to be compared to a string as a type, but at runtime will be validated as a number. Thankfully, in situations this embarrassing TypeScript will give you an error:

Conversion of type 'Validator<number, false, never>' to type 'Validator<string, false, never>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'number' is not comparable to type 'string'.ts

TypeScript will only let you cast when it's plausible. This doesn't mean you can't make a mistake, but it will hopefully save you most of the time. And if you really think you know better than the compiler, you can do v.number() as unknown as Validator<LiterallyAnythingGoesAtThisPoint>, just don't send it to me in a pull request :).

What happens at runtime?

At runtime, it will just be validated with v.string(). These types disappear when the TypeScript is compiled to JavaScript, and you'll observe typeof myField === "string". TypeScript is not static typing. It's some fairy dust sprinkled on top of JavaScript that gets baked off during transpilation to js (e.g. when you run tsc).

What is string branding?

String "branding" is where you annotate a type that is different than a normal string at the type level, even though it's just a string at the runtime level. For instance, a Convex Id<"users"> is the type string & { __tableName: "users" }. This means if I try to assign const foo: Id<"users"> = "bar";, I will get a type error. It's fine to do const foo: string = "bar" as Id<"users">; however, since Id is just a more "refined" type of string.

Build in minutes, scale forever.

Convex is the backend application platform with everything you need to build your project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started