Sanity

TypeScript Best Practices for AI and CMS-Driven Apps

Master TypeScript AI apps with best practices for typing CMS data, LLM APIs, runtime validation, and automation workflows. Write safer, more maintainable AI-powered applications.

June 26, 202610 min readMuhammad Zohaib Ramzan
TypeScript Best Practices for AI and CMS-Driven Apps

TypeScript has become the de facto standard for building robust, production-grade applications — and nowhere is this more apparent than in TypeScript AI apps and CMS-driven architectures. As AI pipelines grow more complex and content models evolve rapidly, strong typing is no longer a luxury; it's a necessity. This post walks through the most important TypeScript best practices for developers building at the intersection of AI, headless CMS, and automation.

Why TypeScript is Essential for AI Apps

AI applications introduce a unique class of runtime uncertainty: LLM responses are dynamic, API contracts shift between model versions, and data flowing through pipelines can be deeply nested or inconsistently shaped. TypeScript addresses these challenges head-on.

Type safety at compile time means you catch mismatches between what your code expects and what an API returns before a single line runs in production. Instead of discovering that response.choices[0].message.content is undefined at 2 AM, your IDE flags it immediately.

IDE support is dramatically improved with TypeScript. Autocomplete, inline documentation, and refactoring tools all depend on accurate type information. In AI apps where you're juggling prompt templates, model configs, and structured outputs, this productivity boost is significant.

Reduced bugs in AI pipelines is perhaps the most compelling argument. A pipeline that ingests a document from a CMS, sends it to an LLM, parses the response, and stores the result has at least four points of potential type failure. TypeScript makes each of those boundaries explicit and verifiable.

Enable strict mode in your tsconfig.json along with noUncheckedIndexedAccess and exactOptionalPropertyTypes — these three settings together catch the vast majority of runtime type errors before they reach production.

Typing CMS Data from Sanity

Sanity is a popular headless CMS for AI-driven apps because of its flexible schema system and real-time APIs. Generating accurate TypeScript types from your Sanity schema eliminates an entire category of bugs.

Generating types with @sanity/typegen is the recommended approach. After defining your schema, run npx sanity typegen generate to produce a sanity.types.ts file with precise types for every document and object in your schema — including union types for polymorphic arrays.

Using @sanity/types gives you access to core Sanity primitives like SanityDocument, Slug, Image, and PortableTextBlock. Import them to annotate your data-fetching functions and keep your interfaces aligned with Sanity's internal structure.

GROQ query typing is where many teams leave type safety on the table. Use defineQuery from next-sanity combined with typegen to get end-to-end typed queries where the return type is inferred automatically from your GROQ projection.

Practical tip: Always project only the fields you need in GROQ queries. This keeps your types narrow and your payloads small — both important for AI apps that process CMS content at scale.

Handling LLM API Types

The OpenAI and Anthropic SDKs ship with TypeScript definitions, but using them effectively requires understanding their structure.

Typing OpenAI responses starts with importing the SDK's own types. The openai package exports OpenAI.Chat.ChatCompletion for standard responses. Always use optional chaining on choices[0]?.message?.content since these fields can be null in certain response scenarios.

Streaming types require a different approach. The SDK exposes Stream<ChatCompletionChunk> for streaming responses. Use for await...of to iterate over chunks and access chunk.choices[0]?.delta?.content for each text fragment.

Error types from the OpenAI SDK extend APIError. Always handle them explicitly using instanceof checks for RateLimitError, AuthenticationError, and the base APIError class. For Anthropic, the @anthropic-ai/sdk package ships full TypeScript definitions including Message, ContentBlock, and streaming types.

Zod for Runtime Validation of AI Responses

TypeScript types are erased at runtime. When an LLM returns a JSON object, TypeScript cannot verify its shape — only Zod (or a similar runtime validator) can. This is critical for TypeScript AI apps that rely on structured outputs.

Why runtime validation matters: LLMs are probabilistic. Even with response_format: { type: 'json_object' }, a model may omit fields, use wrong types, or add unexpected keys. Zod catches these issues before they propagate through your system.

Define a Zod schema for your expected LLM output using z.object() with field-level constraints like z.string().min(1).max(100), z.array(z.string()), and z.enum([...]). Then derive your TypeScript type with z.infer<typeof MySchema> — this keeps your runtime schema and compile-time type perfectly in sync.

.parse() vs .safeParse(): Use .parse() when you want to throw on invalid data (good for internal pipelines), and .safeParse() when you need to handle errors gracefully (good for user-facing features). The .safeParse() result is a discriminated union: { success: true, data: T } or { success: false, error: ZodError }.

Tip: Combine Zod with OpenAI's structured outputs feature for maximum reliability. Define your Zod schema, convert it to JSON Schema with zod-to-json-schema, and pass it as the response_format schema for guaranteed structural compliance.

Generics and Utility Types for Flexible AI Components

As your AI app grows, you'll build reusable components and utilities that need to work across multiple data shapes. TypeScript generics and utility types are your primary tools here.

Generic React components for AI UIs allow you to build type-safe streaming displays, result renderers, and form components. Define an AIResultProps<T> interface with a render: (data: T) => React.ReactNode callback. The component infers T from the result prop, giving you full type safety in the render callback without any explicit type annotations at the call site.

Partial<T> is useful for progressive form state and optimistic updates where not all fields are available yet. Pick<T, K> lets you create focused sub-types from large CMS document types — for example, type PostPreview = Pick<Post, 'title' | 'slug' | 'excerpt' | 'publishedAt'> for list views.

Record<K, V> is ideal for mapping model names to configurations: Record<string, ModelConfig> gives you a typed lookup table for your AI model registry. Conditional types enable powerful type transformations — for example, type StreamOrComplete<T extends boolean> = T extends true ? Stream<ChatCompletionChunk> : ChatCompletion — to model overloaded function signatures cleanly.

Integrating TypeScript with n8n Workflows

n8n is a popular workflow automation tool used to orchestrate AI pipelines. When building custom n8n nodes or webhook integrations, TypeScript types ensure your automation is as reliable as your application code.

Typing webhook payloads prevents silent failures when upstream services change their payload shape. Define a Zod schema for each webhook source — Sanity, Stripe, GitHub — and validate incoming payloads at the edge of your system before they enter your typed application logic.

Custom n8n node types use the n8n-workflow package's type definitions. Implement the INodeType interface and type your execute method with IExecuteFunctions and INodeExecutionData[][] return types for full compile-time safety.

End-to-end type safety in automation workflows means defining shared type packages that both your application and your n8n custom nodes import. This ensures that a change to a Sanity document schema propagates as a type error across your entire automation stack — not a silent runtime failure.

Common Mistakes

  • Using any as an escape hatch. Every any is a hole in your type system. Use unknown instead and narrow the type explicitly.
  • Not validating LLM output at runtime. TypeScript types don't exist at runtime. Assuming an LLM response matches your interface without Zod or similar validation is a guaranteed source of production bugs.
  • Ignoring strict mode. Running TypeScript without "strict": true leaves a large class of null/undefined errors undetected. Enable it from day one — retrofitting it later is painful.
  • Typing CMS responses as any without narrowing. Fetching from Sanity and casting the result to any defeats the purpose of TypeScript. Use typegen or explicit interfaces.
  • Not handling optional chaining on LLM response fields. Fields like response.choices[0]?.message?.content can be null or undefined. Always use optional chaining and provide fallbacks.
  • Forgetting to type async error boundaries. Untyped catch (err) blocks default to unknown in strict mode. Always narrow the error type before accessing its properties.

Best Practices

  • Enable strict mode and noUncheckedIndexedAccess in tsconfig.json from the start of every project. These settings catch the majority of runtime type errors before they reach production.
  • Generate Sanity types with @sanity/typegen and commit the generated file to version control. Re-run generation as part of your CI pipeline whenever the schema changes.
  • Validate all LLM outputs with Zod before using them in application logic. Define schemas that mirror your expected structured outputs and use .safeParse() for graceful error handling.
  • Use the official SDK types for OpenAI, Anthropic, and other AI providers rather than writing your own interfaces. SDK types are maintained and updated with each API version.
  • Create a shared types/ package in monorepos that exports all domain types — CMS document types, AI response types, and webhook payload types. Import from this package everywhere.
  • Avoid type assertions (as SomeType) unless you have validated the data. Prefer type guards and Zod schemas for narrowing.
  • Document complex generic types with JSDoc comments. Generics are powerful but can be opaque — a one-line comment explaining the intent saves future maintainers significant time.

FAQ

Do I need TypeScript if I'm using Zod for validation?

Yes — they serve complementary roles. Zod validates data at runtime; TypeScript catches errors at compile time. Using both gives you defense in depth: TypeScript prevents you from writing code that misuses types, while Zod ensures external data conforms to those types before it enters your system.

How do I handle LLM responses that don't always return the same shape?

Use Zod's z.discriminatedUnion() or z.union() to model variant response shapes. Define a schema for each possible shape and let Zod determine which one matches at runtime. TypeScript will then infer the correct type in each branch of your code.

Should I use interface or type for CMS document types?

Either works, but interface is generally preferred for object shapes that may be extended (e.g., document types that share common fields). Use type for unions, intersections, and computed types. Consistency within a codebase matters more than the choice itself.

How do I keep generated Sanity types in sync with my schema?

Add sanity typegen generate as a pre-build step in your package.json scripts and as a step in your CI pipeline. Some teams also use a Git pre-commit hook to ensure the generated file is always up to date before code is pushed.

Is TypeScript worth the overhead for small AI projects?

Absolutely. The upfront cost of adding TypeScript to a small project is low — especially with modern tooling like Vite, Next.js, and Bun that support TypeScript out of the box. The payoff compounds quickly: even a solo developer benefits from autocomplete, refactoring safety, and self-documenting code.

Building reliable TypeScript AI apps requires more than just adding .ts extensions to your files. It means enabling strict mode, generating types from your CMS schema, validating LLM outputs at runtime with Zod, and designing reusable generic components that scale with your application. By treating type safety as a first-class concern across your entire stack — from Sanity content models to OpenAI responses to n8n webhook payloads — you dramatically reduce the surface area for bugs and make your codebase significantly easier to maintain and extend. Start with the practices in this post, apply them incrementally, and your AI-powered applications will be more robust from day one.