Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use TypeScript strict mode and avoid 'any' type. Convex provides full type safety from database to client - use it!
.claude/skills/kunanonj-cursor-plugin-convex-rule-typescript-strict-no-any/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 137% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 132% | 0% |
| case-06 | ✓→✗ | ▼ Worse | 116% | 0% |
Convex provides end-to-end type safety from your database schema to your client code. Don't throw it away by using any!
json{ "compilerOptions": { // Strict mode (required) "strict": true, // Additional strictness "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, // Modern settings "target": "ES2021", "module": "ESNext", "moduleResolution": "bundler", "skipLibCheck": true } }
anytypescriptexport const processData = mutation({ args: { data: v.any() }, // ❌ No type safety! handler: async (ctx, args) => { const result: any = await doSomething(args.data); // ❌ Lost types! return result; }, });
Problems:
typescriptexport const processData = mutation({ args: { data: v.object({ name: v.string(), age: v.number(), tags: v.array(v.string()), }), }, returns: v.object({ id: v.id("users"), processed: v.boolean(), }), handler: async (ctx, args) => { // args.data is fully typed! const user = await ctx.db.insert("users", { name: args.data.name, age: args.data.age, }); return { id: user, processed: true, }; }, });
Benefits:
Convex generates types for your schema:
typescriptimport { Doc, Id } from "./_generated/dataModel"; // ✅ Use generated types export const getTask = query({ args: { taskId: v.id("tasks") }, returns: v.union( v.object({ _id: v.id("tasks"), title: v.string(), completed: v.boolean(), }), v.null() ), handler: async (ctx, args): Promise<Doc<"tasks"> | null> => { return await ctx.db.get(args.taskId); }, });
typescript// Don't duplicate types - infer from validators const taskValidator = v.object({ title: v.string(), completed: v.boolean(), userId: v.id("users"), }); export const createTask = mutation({ args: taskValidator, handler: async (ctx, args) => { // args is typed from validator! return await ctx.db.insert("tasks", args); }, });
typescript// Define reusable types type TaskStatus = "todo" | "in_progress" | "done"; const statusValidator = v.union( v.literal("todo"), v.literal("in_progress"), v.literal("done") ); export const updateStatus = mutation({ args: { taskId: v.id("tasks"), status: statusValidator, }, handler: async (ctx, args) => { // args.status is TaskStatus (not string) await ctx.db.patch(args.taskId, { status: args.status }); }, });
typescript// Always define return types export const getUser = query({ args: { userId: v.id("users") }, returns: v.union( v.object({ _id: v.id("users"), name: v.string(), email: v.string(), }), v.null() ), handler: async (ctx, args): Promise<Doc<"users"> | null> => { return await ctx.db.get(args.userId); }, });
unknownIf you truly don't know the type, use unknown (not any):
typescript// ✅ Better: unknown (must type-check before use) export const processWebhook = mutation({ args: { payload: v.any() }, // Webhook payloads vary handler: async (ctx, args) => { const payload: unknown = args.payload; // Must type-check before using if ( typeof payload === "object" && payload !== null && "type" in payload && typeof payload.type === "string" ) { // Now payload is narrowed if (payload.type === "user.created") { // Handle user created } } }, });
But prefer proper validation:
typescript// ✅ Best: Proper validation const webhookValidator = v.union( v.object({ type: v.literal("user.created"), userId: v.id("users") }), v.object({ type: v.literal("user.deleted"), userId: v.id("users") }), ); export const processWebhook = mutation({ args: { payload: webhookValidator }, handler: async (ctx, args) => { // Fully typed! if (args.payload.type === "user.created") { // ... } }, });
Add to ESLint config:
javascriptrules: { "@typescript-eslint/no-explicit-any": "error", // Fail on any "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-return": "error", }
typescript// ❌ Bad const data = await fetch(url); const result = (await data.json()) as any;
typescript// ✅ Good - define the type interface ApiResponse { users: Array<{ id: string; name: string }>; } const data = await fetch(url); const result = (await data.json()) as ApiResponse;
typescript// ❌ Bad function processUser(user: any) { return user.name; // No type safety! }
typescript// ✅ Good function processUser(user: Doc<"users">) { return user.name; // Typed! }
typescript// ❌ Bad - using any for "flexibility" export const flexibleUpdate = mutation({ args: { id: v.id("tasks"), data: v.any() }, handler: async (ctx, args) => { await ctx.db.patch(args.id, args.data); // Unsafe! }, });
typescript// ✅ Good - define what's flexible export const updateTask = mutation({ args: { id: v.id("tasks"), title: v.optional(v.string()), completed: v.optional(v.boolean()), priority: v.optional(v.union( v.literal("low"), v.literal("medium"), v.literal("high") )), }, handler: async (ctx, args) => { const updates: Partial<Doc<"tasks">> = {}; if (args.title !== undefined) updates.title = args.title; if (args.completed !== undefined) updates.completed = args.completed; if (args.priority !== undefined) updates.priority = args.priority; await ctx.db.patch(args.id, updates); // Type-safe! }, });
typescript// No autocomplete, no safety const user: any = await ctx.db.get(userId); console.log(user.nam); // Typo! Runtime error ❌
typescriptconst user = await ctx.db.get(userId); if (user) { console.log(user.name); // Autocomplete! ✅ // console.log(user.nam); // Compile error! ✅ }
strict: true in tsconfig.jsonany in code (ESLint enforces)Doc<"tableName"> for database typesId<"tableName"> for ID typesreturns validators on all functions./_generated/dataModelunknown if type truly unknown (rare)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 12,728 | 12,091 | -5% | 1 | 1 | 0% | 3,035 | 5,202 | +71% | 0 | 0 | — |
case-02 | pass→pass | 14,484 | 6,725 | -54% | 1 | 1 | 0% | 2,674 | 3,809 | +42% | 0 | 0 | — |
case-03 | fail→pass | 8,887 | 6,543 | -26% | 1 | 1 | 0% | 2,033 | 3,859 | +90% | 0 | 0 | — |
case-04 | pass→pass | 7,199 | 5,192 | -28% | 1 | 1 | 0% | 1,670 | 3,447 | +106% | 0 | 0 | — |
case-05 | pass→pass | 13,886 | 7,883 | -43% | 1 | 1 | 0% | 2,942 | 4,291 | +46% | 0 | 0 | — |
case-06 | pass→fail | 8,511 | 7,918 | -7% | 1 | 1 | 0% | 1,884 | 4,066 | +116% | 0 | 0 | — |
case-07 | fail→pass | 6,604 | 4,354 | -34% | 1 | 1 | 0% | 1,447 | 3,432 | +137% | 0 | 0 | — |
case-08 | fail→pass | 9,115 | 10,775 | +18% | 1 | 1 | 0% | 2,052 | 4,768 | +132% | 0 | 0 | — |
case-09 | pass→pass | 5,889 | 3,609 | -39% | 1 | 1 | 0% | 1,234 | 2,998 | +143% | 0 | 0 | — |
case-10 | pass→pass | 10,202 | 3,989 | -61% | 1 | 1 | 0% | 2,244 | 3,233 | +44% | 0 | 0 | — |
case-11 | pass→pass | 3,603 | 2,623 | -27% | 1 | 1 | 0% | 721 | 2,909 | +303% | 0 | 0 | — |
case-12 | pass→fail | 9,046 | 6,491 | -28% | 1 | 1 | 0% | 1,856 | 3,837 | +107% | 0 | 0 | — |
case-13 | pass→pass | 4,266 | 2,856 | -33% | 1 | 1 | 0% | 960 | 2,863 | +198% | 0 | 0 | — |
case-14 | pass→pass | 10,426 | 7,850 | -25% | 1 | 1 | 0% | 2,000 | 3,866 | +93% | 0 | 0 | — |
case-15 | pass→pass | 7,503 | 3,741 | -50% | 1 | 1 | 0% | 1,456 | 3,161 | +117% | 0 | 0 | — |
case-16 | pass→pass | 5,561 | 2,449 | -56% | 1 | 1 | 0% | 1,016 | 2,818 | +177% | 0 | 0 | — |
case-17 | pass→pass | 2,779 | 1,305 | -53% | 1 | 1 | 0% | 545 | 2,543 | +367% | 0 | 0 | — |
case-18 | pass→pass | 4,150 | 1,845 | -56% | 1 | 1 | 0% | 805 | 2,670 | +232% | 0 | 0 | — |
case-19 | pass→pass | 4,764 | 3,572 | -25% | 1 | 1 | 0% | 1,040 | 3,031 | +191% | 0 | 0 | — |
case-20 | pass→pass | 6,665 | 4,584 | -31% | 1 | 1 | 0% | 1,372 | 3,322 | +142% | 0 | 0 | — |
case-21 | pass→pass | 8,853 | 10,254 | +16% | 1 | 1 | 0% | 1,848 | 4,647 | +151% | 0 | 0 | — |
case-22 | pass→pass | 8,132 | 9,404 | +16% | 1 | 1 | 0% | 1,708 | 4,187 | +145% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.