TypeScriptでContentBlockのtextエラーが出る原因と直し方
TypeScript/Python SDKでmessage.content[0].textが型エラーになる原因と、typeで絞り込んで安全に読む書き方を解説します。
'text' does not exist on type 'ContentBlock' が出る理由
anthropic.messages.create()のレスポンスからmsg.content[0].textを直接読むと、TypeScriptがProperty 'text' does not exist on type 'ContentBlock'という型エラーを出します。原因はcontent配列の各要素が単一の型ではなく、複数のブロック型を束ねた共用体ContentBlockになっていることです。
エラーが出るのは、絞り込みなしで.textを読む次のようなコードです。
const text = msg.content[0].text;TypeScript SDK v0.131.0・TypeScript 7.0.2でstrictを有効にしたtscの出力は次のとおりです。
npm i @anthropic-ai/sdk typescript
npx tsc --noEmit --strict --target es2022 --module esnext --moduleResolution bundler bad.ts
# bad.ts:9:29 - error TS2339: Property 'text' does not exist on type 'ContentBlock'.
# Property 'text' does not exist on type 'BashCodeExecutionToolResultBlock'.エラーは1件で、2行目はその内訳として共用体のメンバーを1つ挙げているだけです。Issue #432の報告では内訳がToolUseBlock、2026年の追記ではThinkingBlockでした。内訳に出るブロック名は、実際に返ってくるブロックとは関係なく、SDKとコンパイラのバージョンで変わります。
textプロパティを持つのは共用体の12メンバーのうちTextBlockだけです。12メンバーはTextBlock・ThinkingBlock・RedactedThinkingBlock・ToolUseBlock・ServerToolUseBlock・WebSearchToolResultBlock・WebFetchToolResultBlock・CodeExecutionToolResultBlock・BashCodeExecutionToolResultBlock・TextEditorCodeExecutionToolResultBlock・ToolSearchToolResultBlock・ContainerUploadBlockで、いずれもSDKのソース(messages.ts)にある定義です。
いつからこの型エラーが起きるようになったのか
最初の報告は2024年5月31日です。SDKをv0.20.9からv0.22.0へ上げた直後に発生し、報告者によると、msg.content[0].textを無条件に読むコードがアップグレードだけで型エラーになりました。
Issue #432の経過
- 2024年5月31日v0.22.0への更新で型エラーが出ると報告
v0.20.9から上げた直後にmsg.content[0].textが型エラーになった。 - 2024年6月19日メンテナーが意図した変更だと回答
Pull Request #429によるもので、
text以外にツール呼び出しのtool_useも返りうるようになったと説明。 - 2024年6月20日ツール未設定なら`TextBlock`だけを返すオーバーロード案に別のメンテナーも賛成
「意図した変更」と認めたうえで、使い勝手の改善案は気に入っているとコメント。Issueは開いたまま。
- 2025年4月5日型ガード関数のコメントが投稿される
このあと本記事で紹介する
isTextBlockの元になった書き方。 - 2026年5月16日エラーの内訳が`ThinkingBlock`に変わったとの報告
SDK v0.96.0でも同じ絞り込みで直ると書き込まれた。
- 2026年8月5日working as intendedとしてクローズ
戻り値の型を出し分けるオーバーロードは実装されないまま、絞り込みが対処法として示された。
PR #429より前のSDKはレスポンスが常にテキストブロックである前提だったため、.textを無条件に読めました。ツール呼び出し機能の追加でToolUseBlockが混じるようになり、contentの型を単一のTextBlockから共用体へ広げた結果が、この型エラーです。
提案されたオーバーロード案は、ツールを設定していない呼び出しならcontentがTextBlockだけになるよう戻り値の型を出し分けるものでした。提案者のbcherny氏は、ツールを使わない利用者のビルドを壊さずに済むと説明しています。
switch文とif文でブロック型を絞り込む
もっとも直接的な直し方は、typeフィールドでの分岐です。Issue内でメンテナーが最初に示したのはswitch文でした。次のコードは、その形に拡張思考のthinkingを加えたものです。
const response = message.content[0];
switch (response.type) {
case "text":
console.log(response.text);
break;
case "tool_use":
console.log(response.input);
break;
case "thinking":
console.log(response.thinking);
break;
}既定のdisplayではthinkingブロックのthinkingフィールドは空文字なので、この例のcase "thinking"は空行を出力します。
1ブロックだけ判定できればよい場面ではif文でも十分です。クローズ時のコメントで示された、いまの対処法もこの形です。
const block = message.content[0];
if (block.type === "text") {
console.log(block.text);
}型アサーション(as Anthropic.TextBlock)で済ませる方法もIssue内で紹介されています。メンテナーは「ツールを設定していないと分かっている場合」に限り使えるとしたうえで、if文ほど型安全ではないと注記しています。実行時チェックを伴わないので、ツールを1つ追加した直後にモデルがそれを呼ぶと、content[0]はToolUseBlockになります。型アサーションはコンパイル時のチェックを通すだけなので、.textはundefinedのまま後続の処理へ渡ります。
型ガード関数とfilterでtextだけを取り出す
判定を複数箇所で使い回すなら、型ガード関数に切り出す方法がコメント欄で紹介されています。carnivale7898氏が2025年4月5日に投稿したコメントが元になっています。
import type { ContentBlock, TextBlock } from "@anthropic-ai/sdk/resources/messages";
function isTextBlock(block: ContentBlock): block is TextBlock {
return block.type === "text";
}
if (isTextBlock(message.content[0])) {
console.log(message.content[0].text);
}複数のテキストブロックを連結したいだけなら、filterとmapで1行にまとめる書き方があります。2026年5月のコメントで投稿されたgetTextヘルパーで、共用体のメンバーが増えても呼び出し側を変える必要がありません。
const getText = (blocks: ContentBlock[]) =>
blocks
.filter((b): b is TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
const text = getText(message.content);型のimport元には注意が必要です。TextBlockやContentBlockはパッケージのルートからは公開されておらず、@anthropic-ai/sdk/resources/messagesからimportします。v0.131.0でルートからimport type { TextBlock } from "@anthropic-ai/sdk"と書くと、次のエラーでビルドが止まります。
error TS2614: Module '"@anthropic-ai/sdk"' has no exported member 'TextBlock'.
Did you mean to use 'import TextBlock from "@anthropic-ai/sdk"' instead?エラーの文面は「defaultでimportしてはどうか」と案内していますが、直し方はimport元の変更です。絞り込みの書き方が合っていても、import文だけでビルドが落ちる紛らわしいケースになります。
場面ごとの絞り込み方
1ブロックだけ読む
if文が最短です。
block.type === "text"の内側だけで.textが使えます。複数のブロック型を分岐する
switch文にします。
ContentBlockは12メンバーあり増える可能性があるので、未対応の型をdefaultで拾ってログに残せば、追加に気づけます。同じ判定を何度も使う
型ガード関数に切り出します。判定が1か所になり、ブロック型が増えたときの修正漏れを避けられます。
テキストをまとめて連結する
filterとmapのgetTextにします。ブロックの並び順に左右されません。
Pythonでも同じ設計 — content_block.typeで絞り込む
同じ問題はPython SDKでも起きます。anthropic-sdk-pythonのContentBlockも、TypeScript側と同じ12メンバーのUnion型です。
型チェッカーのmypyでresponse.content[0].textを検査すると、textを持たない11メンバーそれぞれについて[union-attr]のエラーが出ます。anthropic 1.11.0・mypyの既定設定での結果です。
pip install anthropic mypy
mypy bad.py
# bad.py:9: error: Item "ThinkingBlock" of "TextBlock | ThinkingBlock | ... | <7 more items>"
# has no attribute "text" [union-attr]
# Found 11 errors in 1 file (checked 1 source file)型エラーを見逃したときの結末
TypeScript
絞り込みを忘れたコードはtscが落とすので、本番に届く前に気づけます。
Python
型チェッカーを併用していなければ、ツール呼び出しを含む応答が返った時点でAttributeErrorになります。CIにmypyやpyrightを入れていないプロジェクトが該当します。
直し方はtype属性での絞り込みで、Issueのコメントに載っているコードがそのまま使えます。
content_block = response.content[0]
if content_block.type == "text":
text = content_block.text
else:
raise ValueError(f"Non-text response received: {content_block.type}")ストリーミングならfinalText()で絞り込みが要らない
テキストだけが欲しいなら、messages.stream()が返すMessageStreamのfinalText()が使えます。型定義のコメントによると、最終メッセージのテキストを、テキストブロックが複数あれば連結した文字列で返します。
const text = await client.messages
.stream({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "hello" }],
})
.finalText();finalText()はPromise<string>を返すので、v0.131.0でもtsc --strictでconst text: string = ...がそのまま通ります。ContentBlockの共用体は戻り値の型に現れないので、絞り込みコードを自分で書く必要がありません。messages.create()の戻り値にはこのメソッドがないため、非ストリーミングでは前節のgetTextを使います。ツール呼び出しのinputなど、テキスト以外のブロックを読みたいときは、finalMessage()でMessageを受け取ってtypeで分岐します。
現行モデルでは既定でcontent[0]がthinkingになる
絞り込みを書いても、content[0]を決め打ちで読む限り別の事故が残ります。Opus 5.5・Sonnet 5.5などの現行モデルでは、thinkingは設定なしで既定で有効です。thinkingのドキュメントによると、1つ以上のthinkingブロックがtextブロックより前に届きます。
さらに、thinking.type: "adaptive"でモデルが単純な依頼の思考を省いたときは、thinkingブロックが作られません。content[0]がThinkingBlockでTextBlockがcontent[1]以降に来るのは、何も設定しない既定の挙動です。displayの既定は"omitted"で、thinkingブロックはthinkingフィールドが空のまま返ります。if (block.type === "text")をcontent[0]にだけ書くと、思考ブロックが先頭に来た応答では何も出力せずに通り抜けます。
adaptiveで思考が省かれるとcontent[0]の型は依頼の難しさでも変わるので、決め打ちは避けます。
よくあるつまずき
- Message Batchesの結果でも絞り込みが必要:
entry.result.message.contentも同じContentBlock型です。絞り込みは本記事のgetTextやtypeでの分岐をそのまま使えます。結果の取り出し方はMessage Batches SDKの実装にあります
まとめ
型エラーの意味は「contentにはテキスト以外のブロックも入る」で、読む前にtypeで絞り込めば消えます。テキストだけが欲しいなら配列全体をfilterするgetTextの形が、ツールや思考ブロックの有無を問わず使えます。
より広くTypeScript SDKの使い方を確認したい場合はClaudeのTypeScript SDKで実装するStreaming・Tool・MCPヘルパー、実行時に発生するAPIエラーとSDK例外クラスの対応はClaude APIのエラー形式とSDK例外クラスの言語別対応表を参照してください。