Claude Media
TypeScriptでContentBlockのtextエラーが出る原因と直し方

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の経過

  1. 2024年5月31日v0.22.0への更新で型エラーが出ると報告

    v0.20.9から上げた直後にmsg.content[0].textが型エラーになった。

  2. 2024年6月19日メンテナーが意図した変更だと回答

    Pull Request #429によるもので、text以外にツール呼び出しのtool_useも返りうるようになったと説明。

  3. 2024年6月20日ツール未設定なら`TextBlock`だけを返すオーバーロード案に別のメンテナーも賛成

    「意図した変更」と認めたうえで、使い勝手の改善案は気に入っているとコメント。Issueは開いたまま。

  4. 2025年4月5日型ガード関数のコメントが投稿される

    このあと本記事で紹介するisTextBlockの元になった書き方。

  5. 2026年5月16日エラーの内訳が`ThinkingBlock`に変わったとの報告

    SDK v0.96.0でも同じ絞り込みで直ると書き込まれた。

  6. 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例外クラスの言語別対応表を参照してください。

この記事を共有:XはてブLinkedIn