DEV Community

KristinZ
KristinZ

Posted 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)