DEV Community

KristinZ
KristinZ

Posted on Edited on

Stop Writing Types by Hand. Infer Them

Adapted from Appendix A of AI Engineering with TypeScript — A Comprehensive Guide to Building AI Agents.
Companion code: github.com/aiengineering-book/code

If you come from the frontend, your TypeScript instinct is probably this: see data, write an interface.

That habit breaks on the backend. You end up with three copies of the same shape — a Zod schema for validation, a Drizzle table for the database, and a hand-written interface gluing them together. They drift. Runtime says one thing, types say another, and you debug ghosts.

The rule: never hand-write a type the library can already give you. Infer it.

1. Zod: infer from the schema

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

// Don't hand-write an interface. Infer it.
type User = z.infer<typeof UserSchema>;

const result = UserSchema.safeParse(unknownInput);
if (result.success) {
  result.data.email; // string — type-safe
}
Enter fullscreen mode Exit fullscreen mode

One source of truth. Change the schema, the type follows. This single habit eliminates an entire class of validation-vs-type drift bugs, which in AI apps — where every LLM output is untrusted JSON — matters a lot.

2. Drizzle: infer from the table

import { pgTable, text, uuid, timestamp } from 'drizzle-orm/pg-core';

const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: text('email').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

type User = typeof users.$inferSelect;    // what SELECT returns
type NewUser = typeof users.$inferInsert; // what INSERT accepts
Enter fullscreen mode Exit fullscreen mode

Note there are two types here, not one — rows coming out of the DB aren't shaped like rows going in (defaults, auto-generated IDs). Hand-written interfaces almost always conflate the two.

3. Hono: infer across the frontend/backend boundary

import { Hono } from 'hono';

const app = new Hono()
  .get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
  .post('/users', async (c) => c.json({ created: true }, 201));

export type AppType = typeof app;
// Frontend: hono/client generates a fully type-safe client from AppType
Enter fullscreen mode Exit fullscreen mode

Your API contract is now compiler-checked end to end. Rename a route, the frontend breaks at build time instead of at 2am in production.

4. The supporting cast: Omit / Pick / ReturnType

Once you infer, you trim with utility types instead of redefining:

type PublicUser = Omit<User, 'passwordHash'>;
type UserPreview = Pick<User, 'id' | 'email'>;
type UpdateUserRequest = Partial<Omit<User, 'id' | 'createdAt'>>;

// And for async functions:
type TaskResult = Awaited<ReturnType<typeof createTask>>;
Enter fullscreen mode Exit fullscreen mode

Partial for PATCH bodies, Omit for stripping secrets, Awaited<ReturnType<...>> for unwrapping promises. No duplication anywhere.

Why this matters more in AI apps

In a normal CRUD app, a type drift is a bug. In an AI app, it's a pipeline failure: LLM returns unstructured text → Zod validates → Drizzle persists → Hono serves → frontend renders. Every boundary is a place where "I thought the shape was X" blows up. Inferring at each layer turns five handshake agreements into one compiler guarantee.


About the book

  • This post is adapted from Appendix A of my book 《AI Engineering with TypeScript — A Comprehensive Guide to Building AI Agents》 at Leanpub

  • The book takes you from server-side TypeScript foundations → LLM APIs and streaming → RAG → ReAct agents and tool calling → MCP → multimodal and production (observability, cost control, prompt-injection defense), closing with a complete AI workbench capstone. 24 chapters, every snippet backed by runnable companion code: github.com/aiengineering-book/code.

  • If this "infer, don't declare" style clicked for you, Chapter 3 (server-side TypeScript patterns) and Chapter 7 (Zod structured output from LLMs) go 10x deeper.

Top comments (0)