TypeScript SDK Type Reference for Content Blocks
@anthropic-ai/sdk types a Claude response's content as a discriminated union of content block types, keyed by a type field. This page is a quick reference for the block types you'll see most often in response.content, and the narrowing pattern TypeScript requires before you can touch a block-specific field.
How to Use This Reference
- Look up the block type by its
.typestring, then copy the narrowing snippet that matches how you're iterating (if-chain vsswitch). - Treat
.typeas the only field you can trust before narrowing. Every other field is specific to one member of the union. - Use the comparison table as a lookup when reading someone else's loop over
response.contentand you need to know what a given block exposes. - Remember there are two related unions:
ContentBlock(what Claude sends you back) and block param types likeToolResultBlockParam(what you send to Claude when constructing messages). They are not interchangeable.
Content Block Types at a Glance
| Type | .type discriminant | Key fields | Where you see it |
|---|---|---|---|
| TextBlock | "text" | .text: string | response.content, the normal assistant reply text |
| ToolUseBlock | "tool_use" | .id: string, .name: string, .input: unknown | response.content, when Claude decides to call a tool |
| ThinkingBlock | "thinking" | .thinking: string | response.content, only when extended thinking is enabled |
The union that types response.content includes other members beyond these three for less common cases (server-side tool activity, redacted thinking, and similar). The three above are the ones a typical response-handling loop narrows on.
TextBlock
TextBlock is Claude's ordinary reply text. It's the block type you'll narrow to most often.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Say hello in one sentence." }],
});
for (const block of response.content) {
if (block.type === "text") {
// TypeScript now knows `block` is a TextBlock here.
console.log(block.text);
}
}ToolUseBlock
ToolUseBlock appears when Claude decides to call one of the tools you provided. .input is typed as unknown because its shape depends on the tool's own input_schema, so you still need to validate or cast it before use.
for (const block of response.content) {
if (block.type === "tool_use") {
// block.id, block.name, block.input are all available here.
console.log(block.id, block.name, block.input);
}
}ThinkingBlock
ThinkingBlock carries Claude's extended thinking output. It only shows up in response.content when extended thinking is turned on for the request; otherwise it's simply absent, not present with an empty value.
for (const block of response.content) {
if (block.type === "thinking") {
console.log(block.thinking);
}
}Narrowing with a switch Statement
An if-chain works, but a switch on block.type reads better once you're handling three or more block types in the same loop, and it keeps each branch's narrowed type isolated:
for (const block of response.content) {
switch (block.type) {
case "text":
console.log(block.text);
break;
case "tool_use":
console.log(block.name, block.input);
break;
case "thinking":
console.log(block.thinking);
break;
default:
// Other block types in the union land here, unnarrowed.
break;
}
}Filtering to One Block Type
When you only care about one block type, filter first instead of branching inside the loop. TypeScript narrows the array element type through a type predicate:
const toolCalls = response.content.filter(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
);
for (const call of toolCalls) {
console.log(call.name, call.input); // no further narrowing needed
}Block Param Types Are a Separate Union
TextBlock, ToolUseBlock, and ThinkingBlock describe what Claude sends back to you. When you construct messages to send to Claude, you use the corresponding param types instead, such as ToolResultBlockParam for returning a tool's result. These param types are not the same TypeScript types as the response blocks, even where the names overlap, so don't reuse a ToolUseBlock you received as if it were valid input content.
const toolResultMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: call.id,
content: "72°F and sunny",
},
],
};FAQs
Why do I need to check block.type before reading block.text?
Because response.content is typed as a union of block types, and only TextBlock has a .text field. TypeScript won't let you access .text on the union type directly; narrowing with block.type === "text" tells the compiler which member of the union you're working with.
What happens if I try to access .text without narrowing first?
A compile-time type error. TypeScript reports that .text doesn't exist on the other members of the ContentBlock union, even if the value would happen to be a TextBlock at runtime.
Is response.content always an array?
Yes. Even a reply that's a single sentence of text comes back as an array with one TextBlock in it. Always iterate or index into it rather than treating response.content as a single block.
What's the difference between ContentBlock and ContentBlockParam types?
ContentBlock types (TextBlock, ToolUseBlock, ThinkingBlock) describe blocks Claude sends you in a response. Param types (TextBlockParam, ToolResultBlockParam, and similar) describe blocks you send to Claude when building a request's messages array. They're separate unions, even when names look similar.
When does a ThinkingBlock actually appear in response.content?
Only when extended thinking is enabled for that request. If thinking isn't enabled, no ThinkingBlock appears at all, so a loop that never matches "thinking" on a non-thinking request is expected behavior, not a bug.
Can a single response contain more than one ToolUseBlock?
Yes, unless parallel tool use is disabled for the request. A response can contain multiple ToolUseBlock entries alongside a TextBlock, so a loop should handle each block independently rather than assuming at most one tool call.
Why is .input typed as unknown on ToolUseBlock instead of something more specific?
Because .input's real shape comes from the input_schema you defined for that tool, which the SDK's static types have no way to know ahead of time. Validate or cast .input against your own schema (for example with Zod) before using its fields.
Should I use an if-chain or a switch statement to narrow block.type?
Either narrows correctly. An if-chain reads fine for one or two types; a switch on block.type scales better once you're branching on three or more, and keeps each case block's narrowed type contained to that branch.
What is ToolResultBlockParam used for?
It's the block you add to a messages array to send a tool's result back to Claude after handling a ToolUseBlock. It's a param type, not a response type, so you build it yourself rather than receiving it from the API.
Do TextBlock, ToolUseBlock, and ThinkingBlock share any common field besides type?
.type is the only field guaranteed across all of them, which is exactly why it's the discriminant. Every other field (.text, .id/.name/.input, .thinking) belongs to just one member of the union.
Will unhandled block types silently break my switch statement?
No, but they'll silently fall through to your default case (or be skipped if you have no default), which can hide new block types added to the union over time. Add a default branch that at least logs the unrecognized block.type so new block types don't disappear unnoticed.
How do I write a function that accepts any content block and narrows internally?
Type the parameter as the block union type (for example Anthropic.ContentBlock), then narrow with the same block.type === "..." checks shown above inside the function body. The union type is what makes narrowing available in the first place.
Does narrowing work with array methods like .filter(), not just for-of loops?
Yes, as long as the filter callback is written as a type predicate ((block): block is Anthropic.ToolUseBlock => ...). A plain boolean-returning callback filters correctly at runtime but doesn't narrow the resulting array's type for TypeScript.
Related
- The Anthropic TypeScript SDK Mental Model - how the client, requests, and responses fit together before you get to individual block types
- TypeScript/JavaScript SDK Basics - installing the SDK and making your first request
- Strongly Typed Tool Definitions with Zod in TypeScript - validating a ToolUseBlock's
.inputagainst a real schema - TypeScript SDK Error Class Reference - the matching reference page for typed error classes instead of content blocks
- Handling Tool Use Requests and Returning Tool Result Blocks - the full round trip from ToolUseBlock to a ToolResultBlockParam response
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
@anthropic-ai/sdkTypeScript SDK (latest release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.