Skip to content

TypeScript as AI Contracts — Guide LLM Code Generation

TypeScript interfaces are machine-readable contracts for AI. Learn the prompting patterns that produce type-safe code from any LLM.

· · 7 min read
TypeScript code with interfaces and type definitions visible in a code editor

Quick Take

The quality of AI-generated TypeScript code correlates directly with the quality of the types you provide. A well-defined interface isn't just documentation, it's a machine-readable contract that forces the AI to generate correct code or produce a compiler error you can fix immediately.

The most common mistake developers make when prompting AI for TypeScript code is asking the AI to determine the data shape. "Write a function that fetches user profile data", and then wondering why the returned object uses different property names than the rest of the codebase. Whichever LLM you reach for, the fix for unreliable code generation is the same.

Types-first prompting flips this. You define exactly what the function should accept and return. The AI's job is to implement the logic that produces that output. TypeScript then verifies the output is actually correct. The AI handles the tedious part; you retain control of the contract.

I've been using this pattern for 14 months across four production TypeScript projects. Here's what I've learned.

TL;DR:

  • Define complete interfaces before prompting, never let AI choose property names or shapes
  • Include the function signature with explicit return type in your prompt
  • TypeScript validates that AI output satisfies your contract at compile time
  • Discriminated unions are the most powerful contract type for AI-generated code

Why AI Guesses Wrong Without Types

Ask an AI to "fetch and display user orders" and it'll generate something like:

// AI-generated without type contract:
async function getUserOrders(userId) {
  const response = await fetch(`/api/orders?user=${userId}`);
  const data = await response.json();
  return data.orders; // returns any[]
}

This looks fine. It compiles without errors on a non-strict config. But now you have an any[] propagating through your codebase. The component that renders the orders will access order.id, order.date, order.amount, and none of those property names came from your actual API response. Maybe your API returns orderId, createdAt, totalCents. You won't find out until runtime.

Now compare what happens with a type contract in the prompt:

// You define the contract:
interface Order {
  orderId: string;
  userId: string;
  createdAt: Date;
  totalCents: number;
  status: 'pending' | 'processing' | 'shipped' | 'delivered';
  items: OrderItem[];
}

interface GetOrdersResult {
  orders: Order[];
  totalCount: number;
  hasMore: boolean;
}

// You write the signature, AI fills in the body:
async function getUserOrders(userId: string): Promise<GetOrdersResult> {
  // AI generates this
}

The AI now generates code with orderId not id, createdAt not date, totalCents not amount. If it doesn't, TypeScript fails the build. You catch the error before it ever reaches a browser.

A person holding a pen over a printed document titled CONTRACT
Photo by Getty Images on Unsplash

The Four Prompting Patterns

Pattern 1: Interface-First Implementation

The base pattern. Always define both the input types and the output type before asking for implementation:

I have these TypeScript types:

[paste your interfaces]

Write [function name]([typed params]): Promise<[return type]> that [business rule in one sentence].

The "business rule in one sentence" is important. Don't describe the implementation, describe the contract behavior. "Returns the 10 most recent orders sorted by createdAt descending, filtered to the given userId." The AI handles the how; you define the what.

Pattern 2: Discriminated Union Contracts

Discriminated unions are the most powerful contract type for AI-generated code because they make impossible states unrepresentable. Define them first:

type ApiResult<T> =
  | { status: 'success'; data: T; timestamp: Date }
  | { status: 'error'; code: string; message: string }
  | { status: 'loading' };

// Prompt: "Write a useOrders hook that returns ApiResult<Order[]>,
// manages the fetch lifecycle, and handles network errors."

When you define a discriminated union, the AI generates code that exhaustively handles all cases, because TypeScript will complain at the use sites if it doesn't. You're forcing correctness through the type system rather than through code review.

Pattern 3: Generic Constraints as Specification

Generics with constraints are dense specifications. Use them for reusable utilities:

// Prompt: "Write a paginate<T extends { id: string }> function
// with this signature:
function paginate<T extends { id: string }>(
  items: T[],
  cursor: string | null,
  limit: number
): { items: T[]; nextCursor: string | null; total: number }

The constraint T extends { id: string } tells the AI exactly what it can assume about the items: they have an id property of type string. It knows it can use item.id for cursor logic. It knows nothing else about T without being told. This is a complete specification in TypeScript syntax.

Pattern 4: Type Guards as Contracts

When you need AI to generate validation code, define the type guard signature first:

function isValidOrder(data: unknown): data is Order {
  // Prompt: "Implement this type guard that validates
  // all required fields from the Order interface above"
}

A type guard with data is Order tells the AI: you must write a function that returns a boolean, and TypeScript knows that true means the argument is safely typed as Order. The implementation has to actually validate the structure, if it returns true too early and your code later accesses order.items, TypeScript trusts the guard. This means the AI is incentivized to write thorough validation.

Handling Third-Party Libraries

The hardest case is AI-generated code that uses third-party libraries with incomplete or overly broad types. Two strategies work:

Strategy 1: Extract narrow types from wide ones

import type { Response } from 'some-library';

// The library's Response is Response<any>. Narrow it:
type TypedResponse<T> = Omit<Response, 'data'> & { data: T };

// Now prompt: "Write a fetchOrders function returning 
// TypedResponse<Order[]> using this-library's client"

Strategy 2: Wrap in your types immediately

// Prompt: "Write fetchRaw that calls the third-party API and
// returns unknown. Then write parseOrderResponse(raw: unknown): Order
// using a Zod schema that validates and transforms the raw response."

The parsing function has an explicit output type (Order). Even if the raw API response is unpredictable, the AI generates validation logic that produces the right output type or throws. Zod makes this pattern clean, see how TypeScript types make AI code reliable for how types and Zod work together.

Building Compound Contracts

Real features need multiple types that reference each other. Define the full graph before prompting:

interface User {
  id: string;
  email: string;
  role: 'admin' | 'member' | 'viewer';
}

interface Permission {
  resource: 'orders' | 'users' | 'settings';
  action: 'read' | 'write' | 'delete';
}

interface AuthContext {
  user: User;
  permissions: Permission[];
  expiresAt: Date;
}

// Now: "Write canPerform(ctx: AuthContext, resource: Permission['resource'],
// action: Permission['action']): boolean that checks ctx.permissions"

With the full type graph in the prompt, the AI knows every relationship: what a User looks like, what a Permission contains, how AuthContext composes them. It generates code that traverses the correct structure. Without this, it guesses, and the guesses are often almost right, which is worse than wrong.

A close-up of a hand signing a stack of paper documents with a pen
Photo by Scott Graham on Unsplash

The Workflow in Practice

My actual workflow for any significant TypeScript feature with AI:

  1. Open a types.ts file and define all interfaces for the feature
  2. Write function signatures with explicit return types but empty bodies
  3. Paste the types + signatures into Claude or Copilot with the business rule
  4. Review the implementation for logic, not types, TypeScript handles type correctness
  5. Run tsc --noEmit as the final check

Step 4 is the key mindset shift. I don't read AI-generated code looking for type mismatches. I trust TypeScript to catch those. I read for logic: does the pagination actually use the cursor correctly? Does the error path return the right discriminated union variant? Is there an off-by-one in the limit calculation?

TypeScript as AI contracts doesn't remove the need for review. It changes what you review, from mechanical type checking to actual reasoning about behavior.

For the quality gate that enforces this workflow on the entire team, see CodeRabbit GitHub Actions.

Frequently Asked Questions

Why do TypeScript types improve AI code generation quality?
AI models generate code by predicting what tokens should follow given the context. When you provide a TypeScript interface, the model has an exact specification of the expected shape: property names, their types, whether they're optional, and relationships between types. This dramatically narrows the prediction space toward correct outputs. Without types, the model guesses at names and shapes, often plausibly wrong.
What should I include in a TypeScript prompt for best AI results?
Include the complete interface or type definition for the return value, any input types, and the function signature with explicit return type annotation. Adding a JSDoc comment with the business rule being implemented improves output quality further. The key is: never ask an AI to determine the shape of data, you define that, then ask the AI to implement the logic.
Does this work with Copilot, Claude, and ChatGPT?
Yes. All major AI coding tools use the surrounding file context to inform suggestions. Copilot reads your open files directly. Claude and ChatGPT respond to types you paste into the prompt. The pattern is the same: define types first, write the function signature with return type, then let the AI fill in the body. TypeScript's explicit syntax makes the contract unambiguous for any model.
What is the difference between a type contract and documentation?
Documentation describes intent in natural language, it can be wrong, outdated, or ambiguous. A TypeScript type contract is enforced by the compiler. If an AI generates code that violates the contract, TypeScript tells you immediately with a specific error. Documentation can lie; types cannot. That's why types-first is a fundamentally different approach than comments-first.