← Blog

Claude API TypeScript: Types and Examples

2026-10-19 · 5 min read · SubToAPI Team

If you're building with TypeScript and want to call Claude without fighting any types or guessing response shapes, this article gives you the actual interfaces you need plus working examples: a basic message request, streaming with async iterators, tool use with typed function schemas, and error handling.

The core problem most TypeScript developers hit is that the Claude API (and most API wrappers around it) doesn't ship a single canonical type definition you can just import and trust completely — SDKs differ slightly in field names, optional properties, and how they type streaming events. Below are the types and examples that match the actual JSON shape of the Messages API, so you can either use them directly with fetch or adapt them to whatever SDK or gateway you're using.

Core request and response types

Here's a minimal but accurate type set for the Messages endpoint:

interface ClaudeMessage {
  role: "user" | "assistant";
  content: string | ContentBlock[];
}

interface ContentBlock {
  type: "text" | "image" | "tool_use" | "tool_result";
  text?: string;
  source?: { type: "base64"; media_type: string; data: string };
  id?: string;
  name?: string;
  input?: Record<string, unknown>;
  tool_use_id?: string;
  content?: string;
}

interface ClaudeRequest {
  model: string;
  max_tokens: number;
  messages: ClaudeMessage[];
  system?: string;
  temperature?: number;
  stream?: boolean;
  tools?: ToolDefinition[];
}

interface ClaudeResponse {
  id: string;
  type: "message";
  role: "assistant";
  content: ContentBlock[];
  model: string;
  stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | "tool_use";
  usage: {
    input_tokens: number;
    output_tokens: number;
  };
}

A basic typed request/response round trip:

async function sendMessage(req: ClaudeRequest): Promise<ClaudeResponse> {
  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(req),
  });

  if (!res.ok) {
    throw new Error(`Request failed: ${res.status} ${await res.text()}`);
  }

  return res.json() as Promise<ClaudeResponse>;
}

const reply = await sendMessage({
  model: "claude-sonnet-4",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Summarize this ticket in one sentence." }],
});

console.log(reply.content[0].text);

This example uses SubToAPI as the endpoint, which exposes Claude through a standard HTTPS API with application keys (sub_live_...), so the request/response shapes below map directly onto what you'd send if you're using your own Anthropic key too — the types are the same either way. Full request/response field docs are at /docs/messages.

Typing streaming responses

Streaming is where TypeScript projects usually get messy because the response is a sequence of server-sent events, not a single JSON object. Here's a typed event union and a reader that handles it cleanly:

type StreamEvent =
  | { type: "message_start"; message: Partial<ClaudeResponse> }
  | { type: "content_block_start"; index: number; content_block: ContentBlock }
  | { type: "content_block_delta"; index: number; delta: { type: "text_delta"; text: string } }
  | { type: "content_block_stop"; index: number }
  | { type: "message_delta"; delta: { stop_reason: string }; usage: { output_tokens: number } }
  | { type: "message_stop" };

async function streamMessage(req: ClaudeRequest, onText: (chunk: string) => void) {
  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ...req, stream: true }),
  });

  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const event = JSON.parse(line.slice(6)) as StreamEvent;

      if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
        onText(event.delta.text);
      }
    }
  }
}

await streamMessage(
  { model: "claude-sonnet-4", max_tokens: 500, messages: [{ role: "user", content: "Write a haiku about TypeScript." }] },
  (chunk) => process.stdout.write(chunk)
);

The discriminated union on type is what makes this usable — TypeScript narrows event.delta correctly once you check event.type === "content_block_delta". Details on event names and ordering are in /docs/streaming.

Typing tool use (function calling)

Tool definitions need their own interface so your handler functions stay type-safe:

interface ToolDefinition {
  name: string;
  description: string;
  input_schema: {
    type: "object";
    properties: Record<string, { type: string; description?: string }>;
    required?: string[];
  };
}

const weatherTool: ToolDefinition = {
  name: "get_weather",
  description: "Get current weather for a city",
  input_schema: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name" },
    },
    required: ["city"],
  },
};

interface WeatherInput {
  city: string;
}

function handleToolUse(block: ContentBlock): string {
  if (block.name === "get_weather") {
    const input = block.input as WeatherInput;
    return `Weather for ${input.city}: 18°C, clear`;
  }
  throw new Error(`Unknown tool: ${block.name}`);
}

When stop_reason is "tool_use", loop over response.content, find blocks with type === "tool_use", call your handler, and send the result back as a tool_result content block in the next message. More patterns and multi-step tool loops are covered in /docs/tools.

Error handling types

Don't let error responses leak as any:

interface ClaudeErrorResponse {
  type: "error";
  error: {
    type: "invalid_request_error" | "authentication_error" | "rate_limit_error" | "overloaded_error" | "api_error";
    message: string;
  };
}

function isClaudeError(payload: unknown): payload is ClaudeErrorResponse {
  return typeof payload === "object" && payload !== null && (payload as any).type === "error";
}

Checking this before casting the response to ClaudeResponse avoids a whole category of runtime crashes in production code.

Getting set up quickly

If you want these types working against a live endpoint without first negotiating direct Anthropic API access, SubToAPI turns your existing Claude access into an HTTPS API with per-application keys, streaming, tool use, and usage metadata — the request and response shapes match what's shown above. Plans start at €9/month for Solo, with Team and Scale tiers for shared seats, and a free trial at /signup. See /docs/quickstart to get a key and run your first typed request in a few minutes.

Questions

Does Anthropic publish an official TypeScript SDK? Yes, Anthropic maintains a TypeScript/JavaScript SDK with its own type exports. The interfaces in this article mirror the same JSON shapes so they work whether you use the SDK, raw fetch, or a gateway like SubToAPI.

How do I type streaming deltas without any? Define a discriminated union keyed on the event's type field, as shown above. TypeScript will narrow each event correctly once you check type in a conditional, giving you safe access to delta.text or usage.output_tokens.

Can I reuse these types with tool use and multi-turn conversations? Yes. ContentBlock covers text, tool_use, and tool_result variants, so the same ClaudeMessage[] array type works for simple chat, tool-calling loops, and image inputs without redefining your message structure.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →