TypeScript for AI Applications: SDKs, APIs, Agents and Structured Outputs
TypeScript is useful in AI applications because model output is uncertain while application contracts must remain explicit. Types help at development time; runtime schemas protect the boundary where external APIs, tools and models can return something unexpected.
This guide focuses on those boundaries rather than general TypeScript syntax. It belongs with the AI application architecture hub and the MCP guides.
Separate provider responses from application types
Do not let an SDK response shape become your domain model. Provider fields and model names change. Translate them at one boundary:
type ModelReply = {
text: string;
model: string;
inputTokens?: number;
outputTokens?: number;
};
function toModelReply(response: ProviderResponse): ModelReply {
return {
text: extractText(response),
model: response.model,
inputTokens: response.usage?.input_tokens,
outputTokens: response.usage?.output_tokens,
};
}
The rest of the application now depends on a stable internal contract. Provider-specific retry headers, stop reasons and tool-call formats remain in the adapter.
Types do not validate model output
This assertion does nothing at runtime:
const result = JSON.parse(raw) as Invoice;
Validate untrusted output with a schema:
import { z } from "zod";
const InvoiceSchema = z.object({
supplier: z.string(),
total: z.number().nonnegative(),
currency: z.string().length(3),
confidence: z.number().min(0).max(1),
});
const parsed = InvoiceSchema.safeParse(JSON.parse(raw));
if (!parsed.success) {
throw new Error("Model returned an invalid invoice shape");
}
Use the same schema to define tool arguments, API responses and evaluation fixtures where possible. Continue with handling invalid model JSON for recovery patterns.
Model tool calls as discriminated unions
Agents should only call known tools with validated arguments:
type ToolCall =
| { name: "lookupOrder"; arguments: { orderId: string } }
| { name: "cancelOrder"; arguments: { orderId: string; reason: string } };
async function execute(call: ToolCall) {
switch (call.name) {
case "lookupOrder":
return lookupOrder(call.arguments.orderId);
case "cancelOrder":
return requestCancellation(call.arguments);
}
}
The exhaustive switch helps when tools are added or renamed. Runtime validation and authorization are still required before executing side effects.
Type streaming events explicitly
A streamed AI response is a sequence of different events, not a partial final object:
type StreamEvent =
| { type: "text.delta"; value: string }
| { type: "tool.call"; id: string; name: string; arguments: unknown }
| { type: "usage"; input: number; output: number }
| { type: "error"; code: string; retryable: boolean }
| { type: "done"; finishReason: string };
Consumers can now handle each event deliberately. Never assume that a network close means a successful done event. The streaming guide covers transport behaviour.
Represent asynchronous jobs as states
Long-running document processing and agent tasks need explicit lifecycle types:
type Job<T> =
| { status: "queued"; id: string }
| { status: "running"; id: string; startedAt: string }
| { status: "succeeded"; id: string; result: T }
| { status: "failed"; id: string; error: string; retryable: boolean };
This prevents a client from treating βno result yetβ as failure or inventing impossible combinations such as a completed job without a result.
Keep secrets server-side
TypeScript cannot make a browser-exposed API key safe. Provider credentials belong in server runtimes, gateways or protected workers. Parse required configuration at startup and fail clearly:
const EnvSchema = z.object({
MODEL_API_KEY: z.string().min(1),
MODEL_BASE_URL: z.string().url(),
});
export const env = EnvSchema.parse(process.env);
Do not prefix secrets with framework conventions that expose them to client bundles. See managing AI API keys and AI security.
Type MCP boundaries, then validate them
MCP clients exchange capabilities, tool definitions and results across a process or network boundary. Generate TypeScript types from a trusted schema where possible, but validate incoming messages before use.
Treat tool descriptions as discovery metadataβnot authorization. The application must still decide which server, user or agent may execute a tool. Review the MCP authentication guide before enabling remote servers.
Avoid false certainty
Useful types describe states the application can actually enforce. Warning signs include:
anyat provider and tool boundaries;- broad assertions after
JSON.parse; - one giant type shared across API, database and UI layers;
- optional fields used to represent unrelated lifecycle states;
- provider model names copied into unions that immediately become stale;
- swallowed validation failures followed by partial execution.
TypeScript makes AI systems safer when it exposes uncertainty and forces boundary checks. It becomes dangerous when a compile-time assertion is mistaken for proof about runtime model behaviour.