convex-schema-validators

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Schema & Validators Guide

Convex 模式与验证器指南

Overview

概述

Convex uses a schema-first approach with built-in validators for type safety. This skill covers schema design, validator patterns, TypeScript type integration, and best practices for type-safe Convex development.
Convex 采用模式优先的设计方法,内置验证器以保障类型安全。本指南涵盖模式设计、验证器模式、TypeScript 类型集成,以及 Convex 类型安全开发的最佳实践。

TypeScript: NEVER Use
any
Type

TypeScript:绝对不要使用
any
类型

CRITICAL RULE: This codebase has
@typescript-eslint/no-explicit-any
enabled. Using
any
will cause build failures.
❌ WRONG:
typescript
const data: any = await ctx.db.get(id);
function process(items: any[]) { ... }
✅ CORRECT:
typescript
const data: Doc<"users"> | null = await ctx.db.get(id);
function process(items: Doc<"items">[]) { ... }
重要规则:此代码库已启用
@typescript-eslint/no-explicit-any
规则。使用
any
会导致构建失败。
❌ 错误示例:
typescript
const data: any = await ctx.db.get(id);
function process(items: any[]) { ... }
✅ 正确示例:
typescript
const data: Doc<"users"> | null = await ctx.db.get(id);
function process(items: Doc<"items">[]) { ... }

When to Use This Skill

适用场景

Use this skill when:
  • Creating or modifying
    convex/schema.ts
  • Defining validators for function arguments and returns
  • Working with document IDs and types
  • Setting up indexes for efficient queries
  • Handling optional fields and unions
  • Integrating Convex types with TypeScript
在以下场景中使用本指南:
  • 创建或修改
    convex/schema.ts
  • 为函数参数和返回值定义验证器
  • 处理文档 ID 和类型
  • 配置索引以实现高效查询
  • 处理可选字段和联合类型
  • 将 Convex 类型与 TypeScript 集成

Schema Definition

模式定义

Basic Schema Structure

基础模式结构

typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    avatarUrl: v.optional(v.string()),
    role: v.union(v.literal("admin"), v.literal("user")),
    createdAt: v.number(),
  })
    .index("by_email", ["email"])
    .index("by_role", ["role"]),

  messages: defineTable({
    authorId: v.id("users"),
    channelId: v.id("channels"),
    content: v.string(),
    isDeleted: v.boolean(),
  })
    .index("by_channel", ["channelId"])
    .index("by_author", ["authorId"])
    .index("by_channel_author", ["channelId", "authorId"]),

  channels: defineTable({
    name: v.string(),
    members: v.array(v.id("users")),
    isPrivate: v.boolean(),
  }),
});
typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    avatarUrl: v.optional(v.string()),
    role: v.union(v.literal("admin"), v.literal("user")),
    createdAt: v.number(),
  })
    .index("by_email", ["email"])
    .index("by_role", ["role"]),

  messages: defineTable({
    authorId: v.id("users"),
    channelId: v.id("channels"),
    content: v.string(),
    isDeleted: v.boolean(),
  })
    .index("by_channel", ["channelId"])
    .index("by_author", ["authorId"])
    .index("by_channel_author", ["channelId", "authorId"]),

  channels: defineTable({
    name: v.string(),
    members: v.array(v.id("users")),
    isPrivate: v.boolean(),
  }),
});

Table Definition Patterns

表定义模式

typescript
// defineTable takes a validator object
defineTable({
  field1: v.string(),
  field2: v.number(),
});

// Chain indexes after defineTable
defineTable({
  userId: v.id("users"),
  status: v.string(),
})
  .index("by_user", ["userId"])
  .index("by_status", ["status"])
  .index("by_user_status", ["userId", "status"]);

// Search indexes for full-text search
defineTable({
  title: v.string(),
  body: v.string(),
}).searchIndex("search_body", {
  searchField: "body",
  filterFields: ["title"],
});
typescript
// defineTable 接收验证器对象
defineTable({
  field1: v.string(),
  field2: v.number(),
});

// 在 defineTable 后链式调用索引
defineTable({
  userId: v.id("users"),
  status: v.string(),
})
  .index("by_user", ["userId"])
  .index("by_status", ["status"])
  .index("by_user_status", ["userId", "status"]);

// 用于全文搜索的搜索索引
defineTable({
  title: v.string(),
  body: v.string(),
}).searchIndex("search_body", {
  searchField: "body",
  filterFields: ["title"],
});

Validator Reference

验证器参考

Primitive Validators

基础类型验证器

typescript
import { v } from "convex/values";

v.string(); // string
v.number(); // number (float64)
v.boolean(); // boolean
v.null(); // null literal
v.int64(); // 64-bit integer (NOT v.bigint() - deprecated!)
v.bytes(); // ArrayBuffer
typescript
import { v } from "convex/values";

v.string(); // 字符串类型
v.number(); // 数字类型(float64)
v.boolean(); // 布尔类型
v.null(); // null 字面量类型
v.int64(); // 64位整数(请勿使用 v.bigint() - 已废弃!)
v.bytes(); // ArrayBuffer 类型

Complex Validators

复杂类型验证器

typescript
// Document IDs
v.id("tableName"); // Id<"tableName">

// Arrays
v.array(v.string()); // string[]
v.array(v.id("users")); // Id<"users">[]
v.array(v.object({ x: v.number() })); // { x: number }[]

// Objects
v.object({
  name: v.string(),
  age: v.number(),
  email: v.optional(v.string()),
});

// Records (string keys, typed values)
v.record(v.string(), v.number()); // Record<string, number>
v.record(v.id("users"), v.string()); // Record<Id<"users">, string>

// Unions (OR types)
v.union(v.string(), v.null()); // string | null
v.union(v.literal("a"), v.literal("b")); // "a" | "b"

// Optionals (field may be missing)
v.optional(v.string()); // string | undefined

// Literals (exact values)
v.literal("active"); // "active" literal type
v.literal(42); // 42 literal type
v.literal(true); // true literal type

// Any (escape hatch - avoid if possible)
v.any(); // any (use sparingly!)
typescript
// 文档ID
v.id("tableName"); // Id<"tableName"> 类型

// 数组
v.array(v.string()); // string[] 类型
v.array(v.id("users")); // Id<"users">[] 类型
v.array(v.object({ x: v.number() })); // { x: number }[] 类型

// 对象
v.object({
  name: v.string(),
  age: v.number(),
  email: v.optional(v.string()),
});

// 记录类型(字符串键,指定类型的值)
v.record(v.string(), v.number()); // Record<string, number> 类型
v.record(v.id("users"), v.string()); // Record<Id<"users">, string> 类型

// 联合类型(或类型)
v.union(v.string(), v.null()); // string | null 类型
v.union(v.literal("a"), v.literal("b")); // "a" | "b" 类型

// 可选字段(字段可能不存在)
v.optional(v.string()); // string | undefined 类型

// 字面量类型(精确值)
v.literal("active"); // "active" 字面量类型
v.literal(42); // 42 字面量类型
v.literal(true); // true 字面量类型

// 任意类型(逃生舱 - 尽可能避免使用)
v.any(); // any 类型(谨慎使用!)

Common Validator Patterns

常用验证器模式

typescript
// Nullable field (can be null)
status: v.union(v.string(), v.null());

// Optional field (may not exist)
nickname: v.optional(v.string());

// Optional AND nullable
deletedAt: v.optional(v.union(v.number(), v.null()));

// Enum-like unions
role: v.union(v.literal("admin"), v.literal("moderator"), v.literal("user"));

// Nested objects
settings: v.object({
  theme: v.union(v.literal("light"), v.literal("dark")),
  notifications: v.object({
    email: v.boolean(),
    push: v.boolean(),
  }),
});

// Array of objects
members: v.array(
  v.object({
    userId: v.id("users"),
    role: v.string(),
    joinedAt: v.number(),
  })
);
typescript
// 可空字段(可以为 null)
status: v.union(v.string(), v.null());

// 可选字段(可能不存在)
nickname: v.optional(v.string());

// 可选且可空
deletedAt: v.optional(v.union(v.number(), v.null()));

// 枚举风格的联合类型
role: v.union(v.literal("admin"), v.literal("moderator"), v.literal("user"));

// 嵌套对象
settings: v.object({
  theme: v.union(v.literal("light"), v.literal("dark")),
  notifications: v.object({
    email: v.boolean(),
    push: v.boolean(),
  }),
});

// 对象数组
members: v.array(
  v.object({
    userId: v.id("users"),
    role: v.string(),
    joinedAt: v.number(),
  })
);

Function Validators

函数验证器

CRITICAL: Every Function MUST Have
returns
Validator

重要提示:每个函数必须定义
returns
验证器

typescript
// ❌ WRONG: Missing returns
export const foo = mutation({
  args: {},
  handler: async (ctx) => {
    // implicitly returns undefined
  },
});

// ✅ CORRECT: Explicit v.null()
export const foo = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    return null;
  },
});
typescript
// ❌ 错误示例:缺少 returns
export const foo = mutation({
  args: {},
  handler: async (ctx) => {
    // 隐式返回 undefined
  },
});

// ✅ 正确示例:显式指定 v.null()
export const foo = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    return null;
  },
});

Query with Validators

带验证器的查询函数

typescript
import { query } from "./_generated/server";
import { v } from "convex/values";

export const getUser = query({
  args: {
    userId: v.id("users"),
  },
  returns: v.union(
    v.object({
      _id: v.id("users"),
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
      role: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});
typescript
import { query } from "./_generated/server";
import { v } from "convex/values";

export const getUser = query({
  args: {
    userId: v.id("users"),
  },
  returns: v.union(
    v.object({
      _id: v.id("users"),
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
      role: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});

Mutation with Validators

带验证器的变更函数

typescript
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const createUser = mutation({
  args: {
    name: v.string(),
    email: v.string(),
    role: v.optional(v.union(v.literal("admin"), v.literal("user"))),
  },
  returns: v.id("users"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("users", {
      name: args.name,
      email: args.email,
      role: args.role ?? "user",
      createdAt: Date.now(),
    });
  },
});
typescript
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const createUser = mutation({
  args: {
    name: v.string(),
    email: v.string(),
    role: v.optional(v.union(v.literal("admin"), v.literal("user"))),
  },
  returns: v.id("users"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("users", {
      name: args.name,
      email: args.email,
      role: args.role ?? "user",
      createdAt: Date.now(),
    });
  },
});

Action with Validators

带验证器的动作函数

typescript
import { action } from "./_generated/server";
import { v } from "convex/values";

export const processImage = action({
  args: {
    imageUrl: v.string(),
    options: v.object({
      width: v.number(),
      height: v.number(),
      format: v.union(v.literal("png"), v.literal("jpeg")),
    }),
  },
  returns: v.object({
    processedUrl: v.string(),
    size: v.number(),
  }),
  handler: async (ctx, args) => {
    // Process image...
    return {
      processedUrl: "https://...",
      size: 1024,
    };
  },
});
typescript
import { action } from "./_generated/server";
import { v } from "convex/values";

export const processImage = action({
  args: {
    imageUrl: v.string(),
    options: v.object({
      width: v.number(),
      height: v.number(),
      format: v.union(v.literal("png"), v.literal("jpeg")),
    }),
  },
  returns: v.object({
    processedUrl: v.string(),
    size: v.number(),
  }),
  handler: async (ctx, args) => {
    // 处理图片...
    return {
      processedUrl: "https://...",
      size: 1024,
    };
  },
});

TypeScript Types

TypeScript 类型

Importing Types

导入类型

typescript
import { Doc, Id } from "./_generated/dataModel";

// Document type for a table
type User = Doc<"users">;
// {
//   _id: Id<"users">;
//   _creationTime: number;
//   name: string;
//   email: string;
//   ...
// }

// ID type for a table
type UserId = Id<"users">;
typescript
import { Doc, Id } from "./_generated/dataModel";

// 表的文档类型
type User = Doc<"users">;
// {
//   _id: Id<"users">;
//   _creationTime: number;
//   name: string;
//   email: string;
//   ...
// }

// 表的ID类型
type UserId = Id<"users">;

Using Types in Code

在代码中使用类型

typescript
import { Doc, Id } from "./_generated/dataModel";

// Function parameter types
async function getUserName(
  ctx: QueryCtx,
  userId: Id<"users">
): Promise<string | null> {
  const user = await ctx.db.get(userId);
  return user?.name ?? null;
}

// Variable types
const users: Doc<"users">[] = await ctx.db.query("users").collect();

// Record with Id keys
const userMap: Record<Id<"users">, string> = {};
for (const user of users) {
  userMap[user._id] = user.name;
}
typescript
import { Doc, Id } from "./_generated/dataModel";

// 函数参数类型
async function getUserName(
  ctx: QueryCtx,
  userId: Id<"users">
): Promise<string | null> {
  const user = await ctx.db.get(userId);
  return user?.name ?? null;
}

// 变量类型
const users: Doc<"users">[] = await ctx.db.query("users").collect();

// 以Id为键的记录类型
const userMap: Record<Id<"users">, string> = {};
for (const user of users) {
  userMap[user._id] = user.name;
}

Context Types

上下文类型

typescript
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";

// Query context - read-only
async function readUser(ctx: QueryCtx, id: Id<"users">) {
  return await ctx.db.get(id);
}

// Mutation context - read and write
async function createUser(ctx: MutationCtx, name: string) {
  return await ctx.db.insert("users", { name, createdAt: Date.now() });
}

// Action context - no db, uses runQuery/runMutation
async function processUser(ctx: ActionCtx, id: Id<"users">) {
  const user = await ctx.runQuery(internal.users.getById, { id });
  // ...
}
typescript
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";

// 查询上下文 - 只读
async function readUser(ctx: QueryCtx, id: Id<"users">) {
  return await ctx.db.get(id);
}

// 变更上下文 - 可读可写
async function createUser(ctx: MutationCtx, name: string) {
  return await ctx.db.insert("users", { name, createdAt: Date.now() });
}

// 动作上下文 - 无数据库直接访问,使用runQuery/runMutation
async function processUser(ctx: ActionCtx, id: Id<"users">) {
  const user = await ctx.runQuery(internal.users.getById, { id });
  // ...
}

Index Design

索引设计

Index Naming Convention

索引命名规范

Include all fields in the index name:
by_field1_and_field2_and_field3
typescript
// Schema
export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
    isDeleted: v.boolean(),
  })
    // ✅ This single index serves THREE query patterns:
    // 1. All messages in channel: .eq("channelId", id)
    // 2. Messages by author in channel: .eq("channelId", id).eq("authorId", id)
    // 3. Non-deleted messages by author: .eq("channelId", id).eq("authorId", id).eq("isDeleted", false)
    .index("by_channel_author_deleted", ["channelId", "authorId", "isDeleted"]),
});

// ❌ REDUNDANT: Don't create by_channel if you have by_channel_author_deleted
// The compound index can serve channel-only queries by partial prefix match
索引名称需包含所有字段:
by_field1_and_field2_and_field3
typescript
// 模式定义
export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
    isDeleted: v.boolean(),
  })
    // ✅ 单个索引可支持三种查询模式:
    // 1. 频道内所有消息:.eq("channelId", id)
    // 2. 频道内某作者的消息:.eq("channelId", id).eq("authorId", id)
    // 3. 频道内某作者未删除的消息:.eq("channelId", id).eq("authorId", id).eq("isDeleted", false)
    .index("by_channel_author_deleted", ["channelId", "authorId", "isDeleted"]),
});

// ❌ 冗余:如果已有by_channel_author_deleted,无需创建by_channel
// 复合索引可通过前缀匹配支持仅按频道查询的场景

Index Usage

索引使用

typescript
// Using indexes in queries
const messages = await ctx.db
  .query("messages")
  .withIndex("by_channel_author_deleted", (q) =>
    q.eq("channelId", channelId).eq("authorId", authorId).eq("isDeleted", false)
  )
  .collect();

// Partial prefix match (uses first field only)
const allChannelMessages = await ctx.db
  .query("messages")
  .withIndex("by_channel_author_deleted", (q) => q.eq("channelId", channelId))
  .collect();
typescript
// 在查询中使用索引
const messages = await ctx.db
  .query("messages")
  .withIndex("by_channel_author_deleted", (q) =>
    q.eq("channelId", channelId).eq("authorId", authorId).eq("isDeleted", false)
  )
  .collect();

// 前缀匹配(仅使用第一个字段)
const allChannelMessages = await ctx.db
  .query("messages")
  .withIndex("by_channel_author_deleted", (q) => q.eq("channelId", channelId))
  .collect();

Validator Extraction from Schema

从模式中提取验证器

Reusing Schema Validators

复用模式验证器

typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

// Define shared validators
export const userValidator = v.object({
  name: v.string(),
  email: v.string(),
  role: v.union(v.literal("admin"), v.literal("user")),
});

export default defineSchema({
  users: defineTable(userValidator),
});
typescript
// convex/users.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import schema from "./schema";

// Extract validator from schema and extend with system fields
const userDoc = schema.tables.users.validator.extend({
  _id: v.id("users"),
  _creationTime: v.number(),
});

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(userDoc, v.null()),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});
typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

// 定义共享验证器
export const userValidator = v.object({
  name: v.string(),
  email: v.string(),
  role: v.union(v.literal("admin"), v.literal("user")),
});

export default defineSchema({
  users: defineTable(userValidator),
});
typescript
// convex/users.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import schema from "./schema";

// 从模式中提取验证器并扩展系统字段
const userDoc = schema.tables.users.validator.extend({
  _id: v.id("users"),
  _creationTime: v.number(),
});

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(userDoc, v.null()),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});

Common Patterns

常用模式

Pattern 1: Status Enum

模式1:状态枚举

typescript
// Schema
const statusValidator = v.union(
  v.literal("pending"),
  v.literal("processing"),
  v.literal("completed"),
  v.literal("failed")
);

export default defineSchema({
  jobs: defineTable({
    status: statusValidator,
    data: v.string(),
  }).index("by_status", ["status"]),
});

// Usage in functions
export const getJobsByStatus = query({
  args: {
    status: v.union(
      v.literal("pending"),
      v.literal("processing"),
      v.literal("completed"),
      v.literal("failed")
    ),
  },
  returns: v.array(
    v.object({
      _id: v.id("jobs"),
      _creationTime: v.number(),
      status: v.string(),
      data: v.string(),
    })
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("jobs")
      .withIndex("by_status", (q) => q.eq("status", args.status))
      .collect();
  },
});
typescript
// 模式定义
const statusValidator = v.union(
  v.literal("pending"),
  v.literal("processing"),
  v.literal("completed"),
  v.literal("failed")
);

export default defineSchema({
  jobs: defineTable({
    status: statusValidator,
    data: v.string(),
  }).index("by_status", ["status"]),
});

// 在函数中使用
export const getJobsByStatus = query({
  args: {
    status: v.union(
      v.literal("pending"),
      v.literal("processing"),
      v.literal("completed"),
      v.literal("failed")
    ),
  },
  returns: v.array(
    v.object({
      _id: v.id("jobs"),
      _creationTime: v.number(),
      status: v.string(),
      data: v.string(),
    })
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("jobs")
      .withIndex("by_status", (q) => q.eq("status", args.status))
      .collect();
  },
});

Pattern 2: Polymorphic Documents

模式2:多态文档

typescript
// Schema with discriminated union pattern
export default defineSchema({
  notifications: defineTable({
    userId: v.id("users"),
    type: v.union(
      v.literal("message"),
      v.literal("mention"),
      v.literal("system")
    ),
    // Common fields
    read: v.boolean(),
    createdAt: v.number(),
    // Type-specific data stored as object
    data: v.union(
      v.object({ type: v.literal("message"), messageId: v.id("messages") }),
      v.object({
        type: v.literal("mention"),
        messageId: v.id("messages"),
        mentionedBy: v.id("users"),
      }),
      v.object({
        type: v.literal("system"),
        title: v.string(),
        body: v.string(),
      })
    ),
  }).index("by_user", ["userId"]),
});
typescript
// 采用区分联合模式的模式定义
export default defineSchema({
  notifications: defineTable({
    userId: v.id("users"),
    type: v.union(
      v.literal("message"),
      v.literal("mention"),
      v.literal("system")
    ),
    // 公共字段
    read: v.boolean(),
    createdAt: v.number(),
    // 类型专属数据存储为对象
    data: v.union(
      v.object({ type: v.literal("message"), messageId: v.id("messages") }),
      v.object({
        type: v.literal("mention"),
        messageId: v.id("messages"),
        mentionedBy: v.id("users"),
      }),
      v.object({
        type: v.literal("system"),
        title: v.string(),
        body: v.string(),
      })
    ),
  }).index("by_user", ["userId"]),
});

Pattern 3: Timestamps Pattern

模式3:时间戳模式

typescript
// Helper for timestamp fields
const timestampsValidator = {
  createdAt: v.number(),
  updatedAt: v.number(),
};

export default defineSchema({
  posts: defineTable({
    title: v.string(),
    body: v.string(),
    authorId: v.id("users"),
    ...timestampsValidator,
  }),
});
typescript
// 时间戳字段的辅助验证器
const timestampsValidator = {
  createdAt: v.number(),
  updatedAt: v.number(),
};

export default defineSchema({
  posts: defineTable({
    title: v.string(),
    body: v.string(),
    authorId: v.id("users"),
    ...timestampsValidator,
  }),
});

Pattern 4: Soft Deletes

模式4:软删除

typescript
export default defineSchema({
  items: defineTable({
    content: v.string(),
    deletedAt: v.optional(v.number()),
  }).index("by_active", ["deletedAt"]),
});

// Query active items only
const activeItems = await ctx.db
  .query("items")
  .withIndex("by_active", (q) => q.eq("deletedAt", undefined))
  .collect();
typescript
export default defineSchema({
  items: defineTable({
    content: v.string(),
    deletedAt: v.optional(v.number()),
  }).index("by_active", ["deletedAt"]),
});

// 仅查询活跃项
const activeItems = await ctx.db
  .query("items")
  .withIndex("by_active", (q) => q.eq("deletedAt", undefined))
  .collect();

Common Pitfalls

常见陷阱

Pitfall 1: Using v.bigint() (Deprecated)

陷阱1:使用v.bigint()(已废弃)

❌ WRONG:
typescript
export default defineSchema({
  counters: defineTable({
    value: v.bigint(), // ❌ Deprecated!
  }),
});
✅ CORRECT:
typescript
export default defineSchema({
  counters: defineTable({
    value: v.int64(), // ✅ Use v.int64()
  }),
});
❌ 错误示例:
typescript
export default defineSchema({
  counters: defineTable({
    value: v.bigint(), // ❌ 已废弃!
  }),
});
✅ 正确示例:
typescript
export default defineSchema({
  counters: defineTable({
    value: v.int64(), // ✅ 使用v.int64()
  }),
});

Pitfall 2: Missing System Fields in Return Validators

陷阱2:返回验证器中缺少系统字段

❌ WRONG:
typescript
export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.object({
    // ❌ Missing _id and _creationTime!
    name: v.string(),
    email: v.string(),
  }),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});
✅ CORRECT:
typescript
export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(
    v.object({
      _id: v.id("users"), // ✅ Include system fields
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});
❌ 错误示例:
typescript
export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.object({
    // ❌ 缺少_id和_creationTime!
    name: v.string(),
    email: v.string(),
  }),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});
✅ 正确示例:
typescript
export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(
    v.object({
      _id: v.id("users"), // ✅ 包含系统字段
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});

Pitfall 3: Using string Instead of v.id()

陷阱3:使用string而非v.id()

❌ WRONG:
typescript
export const getMessage = query({
  args: { messageId: v.string() }, // ❌ Should be v.id()
  returns: v.null(),
  handler: async (ctx, args) => {
    // Type error: can't use string as Id
    return await ctx.db.get(args.messageId);
  },
});
✅ CORRECT:
typescript
export const getMessage = query({
  args: { messageId: v.id("messages") }, // ✅ Proper ID type
  returns: v.union(
    v.object({
      _id: v.id("messages"),
      _creationTime: v.number(),
      content: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.messageId);
  },
});
❌ 错误示例:
typescript
export const getMessage = query({
  args: { messageId: v.string() }, // ❌ 应使用v.id()
  returns: v.null(),
  handler: async (ctx, args) => {
    // 类型错误:无法将string作为Id使用
    return await ctx.db.get(args.messageId);
  },
});
✅ 正确示例:
typescript
export const getMessage = query({
  args: { messageId: v.id("messages") }, // ✅ 使用正确的ID类型
  returns: v.union(
    v.object({
      _id: v.id("messages"),
      _creationTime: v.number(),
      content: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.messageId);
  },
});

Pitfall 4: Redundant Indexes

陷阱4:冗余索引

❌ WRONG:
typescript
export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
  })
    .index("by_channel", ["channelId"]) // ❌ Redundant!
    .index("by_channel_author", ["channelId", "authorId"]),
});
✅ CORRECT:
typescript
export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
  })
    // ✅ Single compound index serves both queries
    .index("by_channel_author", ["channelId", "authorId"]),
});
// Use .eq("channelId", id) for channel-only queries (prefix match)
// Use .eq("channelId", id).eq("authorId", authorId) for both
❌ 错误示例:
typescript
export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
  })
    .index("by_channel", ["channelId"]) // ❌ 冗余!
    .index("by_channel_author", ["channelId", "authorId"]),
});
✅ 正确示例:
typescript
export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
  })
    // ✅ 单个复合索引可支持两种查询
    .index("by_channel_author", ["channelId", "authorId"]),
});
// 使用.eq("channelId", id)查询仅按频道筛选的内容(前缀匹配)
// 使用.eq("channelId", id).eq("authorId", authorId)查询同时满足两个条件的内容

Quick Reference

快速参考

Validator Cheat Sheet

验证器速查表

TypeValidatorTypeScript
String
v.string()
string
Number
v.number()
number
Boolean
v.boolean()
boolean
Null
v.null()
null
64-bit Int
v.int64()
bigint
Bytes
v.bytes()
ArrayBuffer
Document ID
v.id("table")
Id<"table">
Array
v.array(v.string())
string[]
Object
v.object({ x: v.number() })
{ x: number }
Record
v.record(v.string(), v.number())
Record<string, number>
Union
v.union(v.string(), v.null())
string | null
Optional
v.optional(v.string())
string | undefined
Literal
v.literal("active")
"active"
类型验证器TypeScript 类型
字符串
v.string()
string
数字
v.number()
number
布尔值
v.boolean()
boolean
Null
v.null()
null
64位整数
v.int64()
bigint
字节数组
v.bytes()
ArrayBuffer
文档ID
v.id("table")
Id<"table">
数组
v.array(v.string())
string[]
对象
v.object({ x: v.number() })
{ x: number }
记录
v.record(v.string(), v.number())
Record<string, number>
联合类型
v.union(v.string(), v.null())
string | null
可选字段
v.optional(v.string())
string | undefined
字面量类型
v.literal("active")
"active"

Type Import Cheat Sheet

类型导入速查表

typescript
// Document and ID types
import { Doc, Id } from "./_generated/dataModel";

// Context types
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";

// Function builders
import { query, mutation, action } from "./_generated/server";
import {
  internalQuery,
  internalMutation,
  internalAction,
} from "./_generated/server";

// Validators
import { v } from "convex/values";

// Schema builders
import { defineSchema, defineTable } from "convex/server";
typescript
// 文档和ID类型
import { Doc, Id } from "./_generated/dataModel";

// 上下文类型
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";

// 函数构建器
import { query, mutation, action } from "./_generated/server";
import {
  internalQuery,
  internalMutation,
  internalAction,
} from "./_generated/server";

// 验证器
import { v } from "convex/values";

// 模式构建器
import { defineSchema, defineTable } from "convex/server";