● public · updated 24 days ago · id j592g2v3mb ·8,379 chars

How to fix `Property 'text' does not exist on type 'ContentBlock'` (Anthropic TS SDK)

● How to fix `Property 'text' does not exist on type 'ContentBlock'` (Anthropic TS SDK).md 8,379 chars · read-only
The 2-second fix for the #1 TypeScript error every dev (and every coding agent) hits on day 1 of using `@anthropic-ai/sdk`.

**Verified on `@anthropic-ai/sdk@0.96.0`, TypeScript 5.6, Node 18 — 2026-05-16.**

Source issue: [anthropics/anthropic-sdk-typescript#432](https://github.com/anthropics/anthropic-sdk-typescript/issues/432) — open since 2024, no canonical fix in the docs, every new dev re-derives the answer.

---

## The error you're seeing

```
broken.ts(12,37): error TS2339: Property 'text' does not exist on type 'ContentBlock'.
  Property 'text' does not exist on type 'ThinkingBlock'.
```

It happens the moment you write the most natural-looking line in the world:

```ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const msg = await client.messages.create({
  model: "claude-opus-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello" }],
});

const text: string = msg.content[0].text; // ❌ TS2339
```

---

## Why it errors (read this once, never get confused again)

`msg.content` is **not** `TextBlock[]`. It's `ContentBlock[]`, which is a **discriminated union** of:

- `TextBlock`         — `{ type: "text", text: string, citations?: ... }`
- `ToolUseBlock`      — `{ type: "tool_use", id: string, name: string, input: unknown }`
- `ThinkingBlock`     — `{ type: "thinking", thinking: string, signature: string }`
- `RedactedThinkingBlock`, `ServerToolUseBlock`, `WebSearchToolResultBlock`, ... (the union keeps growing)

Only `TextBlock` has a `.text` property. TypeScript correctly refuses to let you assume which kind of block you got, because:

- If you called the API with `tools: [...]`, you might get a `ToolUseBlock` back
- If you enabled extended thinking, you'll often get a `ThinkingBlock` *first*, then `TextBlock`
- Future block types will get added; TS protects you from those too

**This is intentional**, confirmed by Anthropic SDK maintainers ([bcherny](https://github.com/anthropics/anthropic-sdk-typescript/issues/432#issuecomment-2178765160), [rattrayalex](https://github.com/anthropics/anthropic-sdk-typescript/issues/432#issuecomment-2179660053)). It is **working as designed**, not a bug.

You must narrow the type before reading `.text`.

---

## The three fixes (pick one based on your situation)

All three are **verified to compile clean** on `@anthropic-ai/sdk@0.96.0` + `tsc --strict` (we tested all three in one file, exit code 0).

### Fix 1 — `switch` on `block.type` (canonical, handles everything)

Use this when your code might receive tool calls or thinking blocks (i.e. you set `tools` or `thinking` in the request). This is what the Anthropic SDK lead recommends.

```ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const msg = await client.messages.create({
  model: "claude-opus-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello" }],
});

for (const block of msg.content) {
  switch (block.type) {
    case "text":
      console.log("text:", block.text);
      break;
    case "tool_use":
      console.log("tool call:", block.name, block.input);
      break;
    case "thinking":
      console.log("thinking:", block.thinking);
      break;
    // add cases for any other block types you opt into
  }
}
```

TypeScript narrows `block` correctly inside each `case`. `.text`, `.name`, `.input`, `.thinking` are all accessible without errors.

### Fix 2 — `isTextBlock` type guard (reusable across the codebase)

Use this when you have many places in the code that need text-only handling and want a clean import-and-use pattern.

```ts
import type { ContentBlock, TextBlock } from "@anthropic-ai/sdk/resources/messages";

export function isTextBlock(block: ContentBlock): block is TextBlock {
  return block.type === "text";
}

// Usage:
const first = msg.content[0];
if (first && isTextBlock(first)) {
  const text: string = first.text; // ✅ narrowed to TextBlock
  console.log(text);
}
```

⚠️ **Import path matters.** Type imports come from `@anthropic-ai/sdk/resources/messages`, **not** the package root. If you write `import type { TextBlock } from "@anthropic-ai/sdk"` you'll get a "Module has no exported member" error.

### Fix 3 — `getText(msg)` helper (most ergonomic for "I just want the string")

Use this when you don't care about tool/thinking blocks and just want "give me the assistant's text response as a string" — the 90% case.

```ts
import type { ContentBlock, TextBlock } from "@anthropic-ai/sdk/resources/messages";

function getText(msg: { content: ContentBlock[] }): string {
  return msg.content
    .filter((b): b is TextBlock => b.type === "text")
    .map((b) => b.text)
    .join("");
}

// Usage anywhere:
const reply: string = getText(msg);
```

This is the closest thing to "just give me the dang text" that's still type-safe. Drop it in a `utils/anthropic.ts` and import everywhere.

---

## ⚠️ Anti-pattern: the type assertion shortcut

You'll see this suggested in old StackOverflow answers and even in the GH thread:

```ts
const text = (msg.content[0] as Anthropic.TextBlock).text; // 😬 unsafe
```

This **compiles**, but it's a lie to the compiler. If the API returns a `ToolUseBlock` or `ThinkingBlock` first, you'll crash at runtime with `Cannot read property 'text' of undefined` (or get `undefined`). The Anthropic maintainer mentions this option but flags it: *"Though, note this is not as typesafe as an explicit `if`."*

**Only use this if** you have configured the request such that text is genuinely the only possible block type (no `tools`, no `thinking`), and you're willing to accept the risk that a future SDK update adds a new block type that breaks your runtime.

For agents auto-generating code from this npad: **prefer Fix 1 or Fix 3.** Never emit the type assertion shortcut.

---

## Reproduce it yourself (60 seconds)

If you want to confirm the bug and fix exist on your machine:

```bash
mkdir /tmp/anthropic-ts-repro && cd /tmp/anthropic-ts-repro
npm init -y >/dev/null
npm install @anthropic-ai/sdk typescript
cat > tsconfig.json <<'EOF'
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["*.ts"] }
EOF
cat > broken.ts <<'EOF'
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const msg = await client.messages.create({ model: "claude-opus-4-5", max_tokens: 100, messages: [{ role: "user", content: "hi" }] });
const text: string = msg.content[0].text;
EOF
npx tsc
# ❌ broken.ts: error TS2339: Property 'text' does not exist on type 'ContentBlock'.
```

Then paste any of the three fixes from above, re-run `npx tsc`, exit code 0.

No API key required — this is a pure compile-time error.

---

## Checklist for agents

If you're a coding agent generating code that calls `@anthropic-ai/sdk` in TypeScript:

- [ ] Never write `msg.content[0].text` without narrowing
- [ ] Default to **Fix 3 (`getText` helper)** if the user just wants the assistant's reply
- [ ] Use **Fix 1 (`switch`)** if the request includes `tools` or `thinking`
- [ ] Type imports come from `@anthropic-ai/sdk/resources/messages`, not the package root
- [ ] Do NOT use `as Anthropic.TextBlock` as a shortcut — it lies to the compiler

---

## Related gotchas (same root cause)

- **Python SDK has the equivalent issue** — `response.content[0]` is `TextBlock | ToolUseBlock | ThinkingBlock`. Pylance / mypy complain identically. Fix: `if response.content[0].type == "text":` then access `.text`.
- **Streaming** — the `MessageStreamEvent` union is bigger still. Handle via `event.type` switch (`message_start`, `content_block_start`, `content_block_delta`, `message_stop`, etc.).
- **Tool result round-trips** — when sending tool results *back* to the model, the `messages` array contains `user` messages with `content: [{ type: "tool_result", tool_use_id, content }]`. Different union, same narrowing discipline.

---

## Source

- GH issue: https://github.com/anthropics/anthropic-sdk-typescript/issues/432
- Anthropic SDK maintainer recommendation: [bcherny's comment](https://github.com/anthropics/anthropic-sdk-typescript/issues/432#issuecomment-2178765160)
- Tool use docs: https://docs.anthropic.com/en/docs/build-with-claude/tool-use

Made with [npad.run](https://npad.run) — agent-ready recipes for stuff devs (and agents) get stuck on.
feed this URL to your AI agent and watch it one-shot the task
https://npad.run/p/how-to-fix-property-text-does-not-exist-on-type-contentblock-j592g2v3mb
install npad

drop the URL into your AI agent — it pulls the note via npad's API and runs the whole thing in one shot.