Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Achieve end-to-end type safety with Zod runtime validation, tRPC type-safe APIs, Prisma ORM, and TypeScript 5.7+ features. Build fully type-safe applications from database to UI for 2025+ development.
.claude/skills/aiskillstore-type-safety-validation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 101% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 83% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 85% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 103% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 116% | 0% |
End-to-end type safety ensures bugs are caught at compile time, not runtime. This skill covers Zod for runtime validation, tRPC for type-safe APIs, Prisma for type-safe database access, and modern TypeScript features.
When to use this skill:
typescriptimport { z } from 'zod' // Define schema const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), age: z.number().int().positive().max(120), role: z.enum(['admin', 'user', 'guest']), metadata: z.record(z.string()).optional(), createdAt: z.date().default(() => new Date()) }) // Infer TypeScript type from schema type User = z.infer<typeof UserSchema> // Validate data const result = UserSchema.safeParse(data) if (result.success) { const user: User = result.data } else { console.error(result.error.issues) } // Transform data const EmailSchema = z.string().email().transform(email => email.toLowerCase())
Advanced Patterns:
typescript// Refinements const PasswordSchema = z.string() .min(8) .refine((pass) => /[A-Z]/.test(pass), 'Must contain uppercase') .refine((pass) => /[0-9]/.test(pass), 'Must contain number') // Discriminated Unions const EventSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('click'), x: z.number(), y: z.number() }), z.object({ type: z.literal('scroll'), offset: z.number() }) ]) // Recursive Types const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({ name: z.string(), children: z.array(CategorySchema).optional() }) )
typescript// Server: Define procedures import { initTRPC } from '@trpc/server' import { z } from 'zod' const t = initTRPC.create() export const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return await db.user.findUnique({ where: { id: input.id } }) }), createUser: t.procedure .input(z.object({ email: z.string().email(), name: z.string() })) .mutation(async ({ input }) => { return await db.user.create({ data: input }) }) }) export type AppRouter = typeof appRouter // Client: Fully typed! import { createTRPCProxyClient, httpBatchLink } from '@trpc/client' import type { AppRouter } from './server' const client = createTRPCProxyClient<AppRouter>({ links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })] }) // TypeScript knows the exact shape! const user = await client.getUser.query({ id: '123' }) // ^? User | null
prisma// schema.prisma model User { id String @id @default(cuid()) email String @unique posts Post[] profile Profile? createdAt DateTime @default(now()) } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String }
typescriptimport { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() // Fully typed queries const user = await prisma.user.findUnique({ where: { id: '123' }, include: { posts: { where: { published: true }, orderBy: { createdAt: 'desc' } } } }) // user is typed as: User & { posts: Post[] } // Type-safe creates const newUser = await prisma.user.create({ data: { email: 'user@example.com', posts: { create: [ { title: 'First Post', content: 'Hello world' } ] } } })
typescript// Const type parameters (TS 5.0+) function firstElement<T extends readonly any[]>(arr: T) { return arr[0] } const result = firstElement(['a', 'b'] as const) // result is typed as 'a' // Satisfies operator (TS 4.9+) const config = { url: 'https://api.example.com', timeout: 5000 } satisfies Config // Ensures config matches Config, but keeps literal types // Decorators (TS 5.0+) function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const original = descriptor.value descriptor.value = function (...args: any[]) { console.log(`Calling ${propertyKey}`) return original.apply(this, args) } } class API { @logged async fetchData() {} }
typescript// ===== BACKEND (Next.js API) ===== // app/api/trpc/[trpc]/route.ts import { fetchRequestHandler } from '@trpc/server/adapters/fetch' import { appRouter } from '@/server/routers/_app' export async function GET(req: Request) { return fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: () => ({}) }) } export const POST = GET // server/routers/_app.ts import { z } from 'zod' import { prisma } from '@/lib/prisma' import { publicProcedure, router } from '../trpc' export const appRouter = router({ posts: { list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().optional() })) .query(async ({ input }) => { const posts = await prisma.post.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: 'desc' }, include: { author: true } }) return { items: posts.slice(0, input.limit), nextCursor: posts[input.limit]?.id } }), create: publicProcedure .input(z.object({ title: z.string().min(1).max(200), content: z.string().optional() })) .mutation(async ({ input }) => { return await prisma.post.create({ data: input }) }) } }) // ===== FRONTEND (React) ===== // lib/trpc.ts import { createTRPCReact } from '@trpc/react-query' import type { AppRouter } from '@/server/routers/_app' export const trpc = createTRPCReact<AppRouter>() // components/PostList.tsx 'use client' import { trpc } from '@/lib/trpc' export function PostList() { const { data, isLoading } = trpc.posts.list.useQuery({ limit: 10 }) const createPost = trpc.posts.create.useMutation() if (isLoading) return <div>Loading...</div> return ( <div> {data?.items.map(post => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> <span>By {post.author.name}</span> </div> ))} <button onClick={() => createPost.mutate({ title: 'New Post' })}> Create Post </button> </div> ) }
.safeParse() to handle errors gracefullyz.string().brand<'UserId'>())strict: true in tsconfig.jsonnoUncheckedIndexedAccess for safer array accessunknown over anytypeof and ReturnType.parse() for known-good data (faster than .safeParse())| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 18,339 | 20,352 | +11% | 1 | 1 | 0% | 2,702 | 4,954 | +83% | 0 | 0 | — |
case-02 | fail→pass | 16,805 | 23,871 | +42% | 1 | 1 | 0% | 2,682 | 5,382 | +101% | 0 | 0 | — |
case-03 | pass→pass | 11,242 | 12,948 | +15% | 1 | 1 | 0% | 2,085 | 3,864 | +85% | 0 | 0 | — |
case-04 | pass→pass | 12,714 | 17,334 | +36% | 1 | 1 | 0% | 2,155 | 4,367 | +103% | 0 | 0 | — |
case-05 | pass→pass | 10,980 | 11,490 | +5% | 1 | 1 | 0% | 1,905 | 4,112 | +116% | 0 | 0 | — |
case-06 | pass→pass | 11,830 | 5,941 | -50% | 1 | 1 | 0% | 1,196 | 3,360 | +181% | 0 | 0 | — |
case-07 | pass→pass | 17,992 | 23,582 | +31% | 1 | 1 | 0% | 3,201 | 6,427 | +101% | 0 | 0 | — |
case-08 | pass→pass | 19,346 | 17,290 | -11% | 1 | 1 | 0% | 2,393 | 5,025 | +110% | 0 | 0 | — |
case-09 | pass→pass | 18,147 | 17,368 | -4% | 1 | 1 | 0% | 2,508 | 4,531 | +81% | 0 | 0 | — |
case-10 | pass→pass | 12,521 | 7,978 | -36% | 1 | 1 | 0% | 1,384 | 3,872 | +180% | 0 | 0 | — |
case-11 | pass→pass | 4,202 | 10,099 | +140% | 1 | 1 | 0% | 642 | 3,108 | +384% | 0 | 0 | — |
case-12 | pass→pass | 9,161 | 6,224 | -32% | 1 | 1 | 0% | 783 | 3,454 | +341% | 0 | 0 | — |
case-13 | pass→pass | 14,387 | 10,362 | -28% | 1 | 1 | 0% | 1,763 | 4,255 | +141% | 0 | 0 | — |
case-14 | pass→pass | 19,118 | 19,491 | +2% | 1 | 1 | 0% | 2,538 | 5,091 | +101% | 0 | 0 | — |
case-15 | pass→pass | 11,090 | 12,506 | +13% | 1 | 1 | 0% | 1,928 | 3,639 | +89% | 0 | 0 | — |
case-16 | pass→pass | 22,933 | 14,854 | -35% | 1 | 1 | 0% | 3,293 | 5,392 | +64% | 0 | 0 | — |
case-17 | pass→pass | 14,317 | 11,842 | -17% | 1 | 1 | 0% | 1,706 | 3,527 | +107% | 0 | 0 | — |
case-18 | pass→pass | 10,513 | 14,520 | +38% | 1 | 1 | 0% | 1,989 | 4,152 | +109% | 0 | 0 | — |
case-19 | pass→pass | 21,484 | 9,708 | -55% | 1 | 1 | 0% | 2,051 | 4,332 | +111% | 0 | 0 | — |
case-20 | pass→pass | 13,577 | 13,629 | +0% | 1 | 1 | 0% | 1,815 | 4,019 | +121% | 0 | 0 | — |
case-21 | pass→pass | 10,995 | 10,804 | -2% | 1 | 1 | 0% | 2,089 | 4,379 | +110% | 0 | 0 | — |
case-22 | pass→pass | 25,566 | 20,543 | -20% | 1 | 1 | 0% | 3,167 | 5,473 | +73% | 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 +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
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.