or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdauth.mddatabase.mdindex.mdnextjs.mdreact.mdschema.mdserver-functions.mdvalues-validators.md
tile.json

values-validators.mddocs/

Values & Validators

Validator Types

import { v } from 'convex/values';

// Primitives
v.null()
v.boolean()
v.number()    // Float64 (JavaScript number)
v.bigint()    // Int64 (JavaScript bigint)
v.string()
v.bytes()     // ArrayBuffer

// Document IDs
v.id('tableName')

// Structures
v.array(elementValidator)
v.object({ field1: validator1, field2: validator2 })
v.record(v.string(), valueValidator)  // Dynamic keys

// Special
v.optional(validator)     // Field may be undefined
v.nullable(validator)     // Value may be null
v.literal(value)          // Exact value
v.union(v1, v2, ...)      // One of multiple types
v.any()                   // Any value

Examples

Primitives

const userValidator = v.object({
  name: v.string(),
  age: v.number(),
  active: v.boolean(),
  data: v.bytes(),
});

// Usage in function
export const createUser = mutation({
  args: userValidator,  // or args: { name: v.string(), ... }
  handler: async (ctx, args) => {
    // args is typed: { name: string; age: number; active: boolean; data: ArrayBuffer }
  },
});

Arrays

args: {
  tags: v.array(v.string()),
  scores: v.array(v.number()),
  items: v.array(v.object({
    id: v.string(),
    count: v.number(),
  })),
}

Objects

args: {
  user: v.object({
    name: v.string(),
    email: v.string(),
    profile: v.object({
      bio: v.string(),
      avatar: v.optional(v.string()),
    }),
  }),
}

Records (Dynamic Keys)

args: {
  metadata: v.record(v.string(), v.any()),
  scores: v.record(v.string(), v.number()),
  config: v.record(v.string(), v.object({
    enabled: v.boolean(),
    value: v.string(),
  })),
}

// Usage
const metadata = { key1: 'value', key2: 123, key3: true };  // Record<string, any>
const scores = { alice: 100, bob: 95 };  // Record<string, number>

Optional Fields

args: {
  name: v.string(),              // Required
  nickname: v.optional(v.string()),  // May be undefined
  bio: v.optional(v.string()),
}

// In handler
handler: async (ctx, args) => {
  // args.name: string
  // args.nickname: string | undefined
  if (args.nickname) {
    // Use args.nickname
  }
}

Nullable vs Optional

// Optional: field may be absent
v.optional(v.string())  // string | undefined

// Nullable: field is present but may be null
v.nullable(v.string())  // string | null

// Both
v.optional(v.nullable(v.string()))  // string | null | undefined

Unions & Literals

args: {
  status: v.union(
    v.literal('active'),
    v.literal('inactive'),
    v.literal('pending')
  ),  // 'active' | 'inactive' | 'pending'

  result: v.union(
    v.object({ success: v.literal(true), data: v.any() }),
    v.object({ success: v.literal(false), error: v.string() })
  ),
}

Document IDs

args: {
  userId: v.id('users'),
  postId: v.id('posts'),
  commentIds: v.array(v.id('comments')),
  parentId: v.nullable(v.id('items')),  // null or Id<'items'>
}

Type Inference

import { v, Infer } from 'convex/values';

// Define validator
const userValidator = v.object({
  name: v.string(),
  age: v.number(),
  email: v.optional(v.string()),
  tags: v.array(v.string()),
});

// Extract TypeScript type
type User = Infer<typeof userValidator>;
// type User = {
//   name: string;
//   age: number;
//   email?: string;
//   tags: string[];
// }

// Use in code
const user: User = {
  name: 'Alice',
  age: 30,
  tags: ['developer'],
};

Value Type

type Value =
  | null
  | bigint
  | number
  | boolean
  | string
  | ArrayBuffer
  | Value[]
  | { [key: string]: undefined | Value };

// All values stored in Convex must be of type Value
// JavaScript objects with functions, undefined values, etc. are not supported

JSON Conversion

import { convexToJson, jsonToConvex } from 'convex/values';

// Convex to JSON (for serialization)
const value = { count: 123n, data: new ArrayBuffer(8) };
const json = convexToJson(value);
// { count: { "$integer": "123" }, data: { "$bytes": "..." } }

// JSON to Convex (for deserialization)
const value = jsonToConvex(json);
// { count: 123n, data: ArrayBuffer(8) }

Error Handling

import { ConvexError } from 'convex/values';

// Simple error
throw new ConvexError('User not found');

// Structured error with data
throw new ConvexError({
  code: 'VALIDATION_ERROR',
  field: 'email',
  message: 'Invalid email format',
});

// Client-side handling
try {
  await convex.mutation(api.users.create, { email: 'invalid' });
} catch (error) {
  if (error instanceof ConvexError) {
    console.log('Error data:', error.data);
  }
}

Common Patterns

Enum-like Types

// Using union of literals
const STATUS = {
  ACTIVE: 'active',
  INACTIVE: 'inactive',
  PENDING: 'pending',
} as const;

args: {
  status: v.union(
    v.literal(STATUS.ACTIVE),
    v.literal(STATUS.INACTIVE),
    v.literal(STATUS.PENDING)
  ),
}

Discriminated Unions

args: {
  event: v.union(
    v.object({ type: v.literal('click'), x: v.number(), y: v.number() }),
    v.object({ type: v.literal('scroll'), delta: v.number() }),
    v.object({ type: v.literal('submit'), formId: v.string() })
  ),
}

handler: async (ctx, args) => {
  switch (args.event.type) {
    case 'click':
      console.log(args.event.x, args.event.y);
      break;
    case 'scroll':
      console.log(args.event.delta);
      break;
    case 'submit':
      console.log(args.event.formId);
      break;
  }
}

Complex Nested Structures

const addressValidator = v.object({
  street: v.string(),
  city: v.string(),
  state: v.string(),
  zip: v.string(),
  country: v.string(),
});

const userValidator = v.object({
  name: v.string(),
  email: v.string(),
  addresses: v.array(addressValidator),
  preferences: v.object({
    theme: v.union(v.literal('light'), v.literal('dark')),
    notifications: v.object({
      email: v.boolean(),
      push: v.boolean(),
      sms: v.boolean(),
    }),
  }),
  metadata: v.record(v.string(), v.any()),
});

export const updateUser = mutation({
  args: userValidator,
  handler: async (ctx, args) => {
    // Fully typed args
  },
});

Reusable Validators

// Define once
const timestampValidator = v.object({
  createdAt: v.number(),
  updatedAt: v.optional(v.number()),
});

// Reuse
args: {
  post: v.object({
    title: v.string(),
    body: v.string(),
    ...timestampValidator.fields,  // Spread fields
  }),
}