Type-Safe TypeScript
One contract, no drift
One schema → TypeScript types, runtime validators and model constraints together. Type safety removes the sharp boundary between 'the model's output' and 'my code's types'.
What you will learn
- Derive types+validators from one schema
- Eliminate as-casts on model output
- Keep consumers type-aligned as schemas evolve
What is this?
Type-safe TS derives compile-time types and runtime validators from a single schema (Zod works well). Model outputs pass through the validator once, arriving in consumers as precisely-typed values — no as-casts, no schema drift between frontend, backend and eval. The schema is the contract; TS simply enforces it everywhere else.
Why do you need it?
- as-casts on JSON.parse silence errors the validator would have caught.
- One schema drives the model prompt, validator and DTOs — drift is impossible by construction.
- IDE/compile errors find schema consumers when fields change.
- Eval fixtures typed from the same schema catch mismatches early.
Mental model
One master meal plan feeding every kitchen
Shared schema
The chef (model) cooks to the master plan; each kitchen station (module) reads the same plan (schema) to plate correctly. Nobody rewrites the plan per station, so the dish never differs across rooms. A mistyped station now fails loudly instead of silently serving odd portions.
How it works
- Define the schema once (e.g. Zod object).
- Infer the TS type with z.infer.
- Validate parse-time: schema.parse(rawJSON) → typed data or thrown error.
- Use the same schema to build the model's structured-output constraint.
- Consumers import type + validator; compile catches drift.
One schema driving model, runtime and types.
Examples
Schema → type → guard, unified
import { z } from "zod"; export const TicketSchema = z.object({ intent: z.enum(["refund", "complaint", "question"]), confidence: z.number().min(0).max(1), tags: z.array(z.string()).max(10),});export type Ticket = z.infer<typeof TicketSchema>; // model call uses TicketSchema; consumers use Ticket typefunction handle(rawJson: unknown): Ticket { return TicketSchema.parse(rawJson); // throws on mismatch, typed afterwards}The schema feeds the model constraint AND runtime validation AND static types.
In production
- Route handlers parse LLM blobs via the schema → typed handlers.
- React UIs receive Ticket types, rendering enums as typed components.
- Eval suites compare generated data with the shared schema.
Best practices
- Single source of schema truth per system boundary.
- Prefer schema.parse over safeParse + as-cast where failures must raise.
- Export types from schema modules, never redefine manually.
- Generate fixtures/tests from the schema when it changes.
Common mistakes
- Type = hand-copy of schema; validator = another copy; prompt = third — three truths.
- as/unknown casts that void the validator's guarantees.
- Optional-vs-nullable confusion leaking through types.
Common issues
Check: typed boundaries but 'any' everywhereMissing validators at ingestion
Enforce parse at the API boundary; forbid implicit any on model output.
Schema changes break the frontendConsumers not regenerated/imported
Derive all consumers from the shared schema module.
Debugging tips
- Type error after schema edit → consumers compile-free because they use z.infer.
- Runtime error unreachable in types → double check a cast is not lurking.
- UI shows raw model blob (any) → the route dropped the schema at the boundary.
Interview checkpoint
Hands-on lab
- Define a Zod schema
- Infer the type and validate a payload
- See the compile error move with the schema
Schema → Type → Guard One-Pager
30 min · intermediate
Quick quiz
z.infer derives…
Trusting a cast instead of a validator risks typed-but-wrong data.