Found while compiling packages/plugin-chatbot/README.md's snippets for objectui#5174 batch 30 (PR to follow). Filed unassigned and NOT fixed there — that batch may not touch packages/plugin-chatbot/src/**.
Note on spelling: generic type arguments are written as WORDS below (UIMessage of unknown, UIDataTypes, UITools) because GitHub's body sanitizer eats tag-shaped fragments, backticks and code fences included, and this card is entirely about a generic type.
What
packages/plugin-chatbot/src/mapMessages.ts exports uiMessagesToChatMessages / uiMessageToChatMessage. Its own docblock states the purpose:
Shared between useObjectChat (which composes useChat internally) and apps that drive useChat themselves (e.g. Studio, which needs a custom prepareSendMessagesRequest transport).
and packages/plugin-chatbot/README.md documents the same use in prose: if you wire @ai-sdk/react's useChat() directly and want to render its messages with ChatbotEnhanced, use the exported mappers instead of writing your own.
The declared parameter type does not accept that input.
Measured
Compiling the README's mapper snippet against the built packages/plugin-chatbot/dist/index.d.ts and the installed @ai-sdk/react@4.0.68 (its own declared dependency), with scripts/check-doc-snippet-types.mjs's analyzer, produces exactly one diagnostic:
TS2345: Argument of type 'UIMessage[]' is not assignable to parameter of type 'AnyUIMessage[]'.
Types of property 'parts' are incompatible.
Type 'UIMessagePart[]' is not assignable to type 'AnyPart[]'.
Type 'TextUIPart' is not assignable to type 'AnyPart'.
Types of property 'state' are incompatible.
Type '"done" | "streaming" | undefined' is not assignable to type
'"input-streaming" | "input-available" | "approval-requested" |
"approval-responded" | "output-available" | "output-error" |
"output-denied" | undefined'.
The mechanism, in one line: mapMessages.ts:19 declares a deliberately loose local AnyPart, but types one of its members tightly —
state?: ChatToolInvocation['state'];
That union is the TOOL-invocation state set. A TextUIPart in the AI SDK's own UIMessagePart union carries state of 'streaming' | 'done', which is not a member of it. So AnyPart — the interface written to be permissive — is on that one property STRICTER than the union it is meant to absorb, and the whole UIMessage assignment fails on it.
Why nothing caught it
The package's own call site does not type-check this assignment. packages/plugin-chatbot/src/useObjectChat.ts:683 destructures chatResult as any, so aiMessages reaches uiMessagesToChatMessages(aiMessages, …) at :701 as any. The unit test packages/plugin-chatbot/src/__tests__/mapMessages.test.ts:135 calls it as uiMessagesToChatMessages(msgs as never, …). Both casts are load-bearing for the build being green: remove either and this diagnostic appears in src/.
The README snippet is the first place in the repository where a caller passes useChat's real return value to this export with no cast, which is why the doc-snippet gate is where it surfaced.
Reachability, honestly
No runtime effect — the mapper duck-types on p.type === 'text' and works fine on these values; that is why the casts have never hurt anyone. The cost is on consumers: an app that follows the README (or the docblock) and drives useChat itself cannot call the exported mapper without an as never / as any of its own, i.e. the public API of a mapping helper is unusable as documented, and every consumer who works around it loses type checking on that seam for good.
Options
- A. Widen
AnyPart.state to string (or to the union of both state sets) in mapMessages.ts. Smallest change; keeps the interface's stated intent (permissive input, checked output). The reads in that file already branch on p.type before touching state.
- B. Type the parameter against the SDK's own
UIMessage from ai. Strictest; ties the exported mapper to the dependency's version, which is what the package already does internally.
- C. Leave it and document the cast. Rejected on the contract-first rule (AGENTS.md #0.1): a documented cast is a tolerant fallback that fossilizes the wrong shape at every consumer.
Recommendation: A, then delete the doc-snippet: fragment marker the objectui#5174 batch-30 PR has to put on that README block — the block is what a reader should write and compiles the day this lands. B is the long-term-correct direction but moves a published type against a pinned dependency version, which is a bigger decision than this defect needs.
Related: objectui#5174 (the gate that measured it), objectui#4424 (the ObjectChatMessage contract this mapper feeds), objectui#4399 (the previous messages as any on the same seam, which that card removed for the renderers).
Filed by the domain:devx @ objectui execution seat's batch-30 dev, from Claude Code session session_01FhBNJcLRZLe8M87VcUgpKr.
Found while compiling
packages/plugin-chatbot/README.md's snippets for objectui#5174 batch 30 (PR to follow). Filed unassigned and NOT fixed there — that batch may not touchpackages/plugin-chatbot/src/**.Note on spelling: generic type arguments are written as WORDS below (
UIMessage of unknown, UIDataTypes, UITools) because GitHub's body sanitizer eats tag-shaped fragments, backticks and code fences included, and this card is entirely about a generic type.What
packages/plugin-chatbot/src/mapMessages.tsexportsuiMessagesToChatMessages/uiMessageToChatMessage. Its own docblock states the purpose:and
packages/plugin-chatbot/README.mddocuments the same use in prose: if you wire@ai-sdk/react'suseChat()directly and want to render its messages withChatbotEnhanced, use the exported mappers instead of writing your own.The declared parameter type does not accept that input.
Measured
Compiling the README's mapper snippet against the built
packages/plugin-chatbot/dist/index.d.tsand the installed@ai-sdk/react@4.0.68(its own declared dependency), withscripts/check-doc-snippet-types.mjs's analyzer, produces exactly one diagnostic:The mechanism, in one line:
mapMessages.ts:19declares a deliberately loose localAnyPart, but types one of its members tightly —That union is the TOOL-invocation state set. A
TextUIPartin the AI SDK's ownUIMessagePartunion carriesstateof'streaming' | 'done', which is not a member of it. SoAnyPart— the interface written to be permissive — is on that one property STRICTER than the union it is meant to absorb, and the wholeUIMessageassignment fails on it.Why nothing caught it
The package's own call site does not type-check this assignment.
packages/plugin-chatbot/src/useObjectChat.ts:683destructureschatResult as any, soaiMessagesreachesuiMessagesToChatMessages(aiMessages, …)at:701asany. The unit testpackages/plugin-chatbot/src/__tests__/mapMessages.test.ts:135calls it asuiMessagesToChatMessages(msgs as never, …). Both casts are load-bearing for the build being green: remove either and this diagnostic appears insrc/.The README snippet is the first place in the repository where a caller passes
useChat's real return value to this export with no cast, which is why the doc-snippet gate is where it surfaced.Reachability, honestly
No runtime effect — the mapper duck-types on
p.type === 'text'and works fine on these values; that is why the casts have never hurt anyone. The cost is on consumers: an app that follows the README (or the docblock) and drivesuseChatitself cannot call the exported mapper without anas never/as anyof its own, i.e. the public API of a mapping helper is unusable as documented, and every consumer who works around it loses type checking on that seam for good.Options
AnyPart.statetostring(or to the union of both state sets) inmapMessages.ts. Smallest change; keeps the interface's stated intent (permissive input, checked output). The reads in that file already branch onp.typebefore touchingstate.UIMessagefromai. Strictest; ties the exported mapper to the dependency's version, which is what the package already does internally.Recommendation: A, then delete the
doc-snippet: fragmentmarker the objectui#5174 batch-30 PR has to put on that README block — the block is what a reader should write and compiles the day this lands. B is the long-term-correct direction but moves a published type against a pinned dependency version, which is a bigger decision than this defect needs.Related: objectui#5174 (the gate that measured it), objectui#4424 (the
ObjectChatMessagecontract this mapper feeds), objectui#4399 (the previousmessages as anyon the same seam, which that card removed for the renderers).Filed by the
domain:devx @ objectuiexecution seat's batch-30 dev, from Claude Code sessionsession_01FhBNJcLRZLe8M87VcUgpKr.