Skip to content

Commit 2ad137b

Browse files
tombeckenhamclaudeautofix-ci[bot]
authored
chore(types): tighten TypeScript safety — strict flags, typed linting, public-API type tests, double-cast ban (#564) (#579)
* test(types): add public-API type-test coverage for chat() and InferChatMessages Lock in the per-model provider-options gating across `chat()` and the ai-client → useChat type flow before tightening the compiler so regressions surface as compile failures rather than runtime drift. Covers issue #564 (Stage 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(types): enable strict TS flags noImplicitOverride, noPropertyAccessFromIndexSignature, noFallthroughCasesInSwitch Tighten the compiler baseline per issue #564 by flipping three flags that are mechanical to absorb. Fix the surfaced fallout: - Add `override` modifiers to adapter classes whose `kind` / `chatStream` / `summarizeStream` members shadow the base abstractions. - Convert dynamic `obj.foo` reads on `Record<*, *>` / index-signature carriers (provider SDK envelopes, JSON-schema walks, devtools wrappers) to bracket notation. - Replace the `Record<string, any>` parameter on `makeStructuredOutputCompatible` with `JSONSchema`, and introduce `toJsonSchema()` so the Standard-Schema → JSONSchema bridge no longer needs `as` casts to bypass index-signature variance. - Bracket Vue template `$slots.X` lookups so vue-tsc accepts them. `exactOptionalPropertyTypes` and `useDefineForClassFields` from the original five-flag set are deferred — the former is the dominant churn (~250 sites) and warrants its own coordinated PR per the issue's review guidance, and the latter breaks vitest's mock-hoisting transform in adapter tests that construct classes with field initializers. Covers issue #564 (Stage 2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(lint): add typed ESLint rules for streaming/agent-loop correctness Add a new \`tanstack/ai/typed\` block scoped to packages/typescript/*/src/** that enables eight typed-linting rules per issue #564: - error: no-floating-promises, no-misused-promises, await-thenable, switch-exhaustiveness-check, consistent-type-exports - warn: no-explicit-any, no-non-null-assertion, prefer-readonly Also inverts ban-ts-comment from the base \`@tanstack/eslint-config\` — \`@ts-ignore\` is now disallowed outright and \`@ts-expect-error\` must carry a description. Fixes the fallout each rule surfaced: - Floating promises: prefix \`sendMessage()\` / \`addToolApprovalResponse()\` fire-and-forget calls in the React/Solid UI shells with \`void\`. These are intentionally async at the client layer but synchronous from a DOM event handler's perspective. - Misused promises: widen \`errorState.reload\` callback type from \`() => void\` to \`() => Promise<void>\` in chat-messages props, since the chat client's \`reload()\` is async. - Exhaustive switches: add \`case undefined:\` co-cases for grok realtime events and elevenlabs codec parsing, enumerate the remaining Anthropic stop_reason variants, add explicit cases for video/document content parts in the openai-base responses adapter, add the missing \`thinking\` color in ai-devtools, and extract \`reason\` to a local variable in IterationCard so the discriminator stays in scope. - consistent-type-exports: switch openai-base \`./tool-choice\` to \`export type *\`. Covers issue #564 (Stage 3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: fail CI on @ts-ignore / @ts-nocheck in library source Adds `scripts/check-ts-suppressions.mjs` (a one-shot Node walker that greps every `packages/typescript/<pkg>/src/**.{ts,tsx}` for forbidden suppressions) plus a `test:no-suppressions` script wired into `test:pr` and `test:ci`. `@ts-expect-error` remains allowed — the linked typed-ESLint block requires a description on it, and the variant self-heals when the underlying error disappears. `@ts-ignore` silently rots. Issue #564 baseline is zero matches today; adopt the guard so future regressions surface in CI rather than during incident triage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: add changeset for TypeScript safety tightening (#564) Patch bump across every workspace package touched by the strict-flags / typed-linting / suppression-guard work. Public API surface is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(types): add brandProviderTool helper and use vendor types directly in Anthropic adapter Collapses 16 `as unknown as <ProviderTool>` phantom-brand casts across Anthropic, Gemini, and OpenRouter tool factories into a single audited `brandProviderTool<T>()` helper in @tanstack/ai. The helper uses `as T` (not `as unknown as T`) so TS's structural-overlap check still applies — future refactors that break the brand contract surface here instead of at distant call sites. Also refactors the Anthropic text adapter to import `ContentBlockParam`, `ThinkingBlockParam`, and `ToolUseBlockParam` directly from `@anthropic-ai/sdk/resources/messages` and pushes thinking-block construction into a properly-typed builder. Eliminates the `as unknown as AnthropicContentBlock` cast that was hiding a TS narrowing limitation rather than a real type mismatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(types): ban `as unknown as` double-casts in library source Sweeps the unaudited `as unknown as <Type>` double-cast pattern out of library source across 14 packages, then adds a `no-restricted-syntax` rule to prevent regression. Why: `as unknown as T` bypasses TypeScript's structural-overlap check (the safety net that errors when two types don't sufficiently overlap) — equivalent to `@ts-ignore` for type assertions. Plain `as T` keeps the check, so refactors that break a cast's structural premise surface at the cast site instead of at distant call sites. Per-package results: - 13 sites reduced to plain `as T` (ai, ai-client, ai-fal, ai-utils) - 2 bug fixes where the cast was hiding real wrong behavior: - ai-gemini: JSON.parse fallback was casting a `string` to `Record<string, unknown>`. Replaced with `parsedArgs = {}`. - ai-ollama: unreachable defensive branch was casting `never` to `Record<string, unknown>`. Replaced with `{}` (matches the JSON-parse failure fallback in the same function). - 12 sites gated with `// eslint-disable-next-line no-restricted-syntax -- <reason>` after `as T` failed the structural-overlap check. Reasons cluster around: conditional return types (4 framework hook `useChat`/`createChat`), DOM/TS-lib limitations (`String.fromCharCode .apply(Uint8Array)`, `Uint8Array.fromBase64` Stage-3 proposal), discriminated-union construction (OpenRouter `NormalizedStreamEvent` built field-by-field), and a `RTCErrorEvent` duck-type. Lint rule scoped to `packages/typescript/*/src/**/*.{ts,tsx}` plus `src/**/*.{ts,tsx}` to also fire under the 4 packages (ai, ai-client, ai-svelte, ai-fal) that ship a local `eslint.config.js` re-exporting root — flat-config evaluates `files` globs relative to the config-file's directory, so the original glob silently misses those packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * refactor(lint): close typed-linting coverage gap in 4 packages Four packages (\`ai\`, \`ai-client\`, \`ai-svelte\`, \`ai-fal\`) shipped a no-op local \`eslint.config.js\` that re-exported root with empty rules. Flat-config evaluates \`files\` globs relative to the config-file's directory, so the root's typed-linting block \`packages/typescript/*/src/**\` glob silently resolved to nothing in those packages — the strict rules (no-floating-promises, no-misused-promises, switch-exhaustiveness-check, consistent-type-exports, ban-ts-comment override) didn't apply there. Removes the four no-op local configs and fixes the violations that surfaced: - ai/src/middlewares/otel.ts:118 — \`switch (type)\` missing \`case undefined:\`. Added with \`[unknown]\` placeholder; dropped the now-redundant \`?? 'unknown'\` from the default branch. - ai/src/activities/summarize/chat-stream-summarize.ts:214 — \`switch (options.style)\` missing \`undefined\`. Added a case that mirrors the existing default-prompt text. - ai/src/activities/chat/index.ts:695 and stream/processor.ts:456 — switches on AG-UI \`EventType\` where case labels are string literals (\`'TEXT_MESSAGE_START'\` etc.) but the discriminant is the \`EventType\` enum. The rule's exhaustiveness analysis treats those as unrelated types and flags every variant as unmatched even though the default already handles them. Scoped \`eslint-disable-next-line\` with reason on each switch. - ai/src/realtime/index.ts:4 — \`export * from './types'\` → \`export type * from './types'\` (all exports are types). - ai-client/src/generation-client.ts:162 and video-generation-client.ts:188 — \`switch (event.type)\` on AG-UI \`EventType\`; consumer only handles the generation-lifecycle subset (\`CUSTOM\`, \`RUN_FINISHED\`, \`RUN_ERROR\`). Rule's \`considerDefaultExhaustiveForUnions\` defaults to false. Added \`default: break\` plus \`eslint-disable-next-line\` with reason. - ai-client/src/realtime-client.ts:171,308,347 — fire-and-forget promise calls (\`startListening\` kicks off audio capture, \`destroy\` calls \`disconnect\`, refresh-token timer). Prefixed with \`void\` to make intent explicit. - ai-client/src/realtime-client.ts:412 — \`no-misused-promises\` on an async event handler passed to a \`() => void\` slot. Converted to a sync arrow with an inner \`void (async () => {...})()\` IIFE; hoisted \`tool.execute\` to a local const to preserve narrowing across the IIFE boundary. - ai-fal/src/utils/client.ts:86 — \`switch (ext)\` on \`string | undefined\`. Added \`case undefined:\` that falls through to the existing default \`return 'audio/mpeg'\`. Doesn't address the warnings (~298 across the four packages, mostly \`no-explicit-any\` and \`no-non-null-assertion\` — both configured as warnings in the root). Those are a separate cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(examples): add test:types to all examples and fix surfaced type errors Before this commit, of 9 examples only \`ts-react-media\` had a \`test:types\` script. Most ran \`"test": "exit 0"\` and never had their types checked by CI even though every example ships a \`tsconfig.json\` with strict mode. That gap is now closed. For each example: - \`ts-react-chat\` — added \`tsc --noEmit\`. Fixed 4 ElevenLabs adapter calls passing a generic \`string\` where a literal-union model id was expected (\`as 'eleven_multilingual_v2'\` etc.), renamed 4 unused middleware \`ctx\` params to \`_ctx\`, and tightened the transcription generation route's fetcher input to the \`TranscriptionGenerateInput\` shape from \`@tanstack/ai-client\`. - \`ts-vue-chat\` — added \`vue-tsc -b\` (already used in \`build\`). No source fixes needed. - \`ts-solid-chat\` — added \`tsc --noEmit\`. Excluded the generated \`src/routeTree.gen.ts\` and three orphan files (\`src/lib/stub-adapter.ts\`, \`src/lib/stub-llm.ts\`, \`src/utils/demo.tools.ts\` — the first two reference outdated \`@tanstack/ai\` types; the third imports Vercel's \`ai\` package and is unreferenced). Fixed \`api.chat.ts\` to pass the \`serverTools\` array directly to \`mergeAgentTools\` instead of wrapping it in a \`Record\`. - \`ts-svelte-chat\` — added \`svelte-kit sync && svelte-check\`. Same \`mergeAgentTools\` array fix as solid. Split the per-provider \`chat()\` call into a ternary so each branch resolves to a concrete adapter type (spreading the union erased the generic). - \`vanilla-chat\` — created a \`tsconfig.json\` (didn't exist), scoped to the example's \`src/**/*.js\` + \`vite.config.ts\` with \`allowJs: true\` and \`checkJs: false\` so \`tsc\` doesn't surface unrelated JS drift. Added \`tsc --noEmit\`. - \`ts-group-chat\` — added \`tsc --noEmit\`. Removed a dead \`private webSocket\` field, replaced an old AG-UI \`'content'\` stream-chunk discriminator with \`'TEXT_MESSAGE_CONTENT'\`, and typed a tool handler's \`unknown\` args via a single named alias. - \`ts-react-search\` — added \`tsc --noEmit\`. No source fixes needed (tsconfig already excludes generated files). - \`ts-code-mode-web\` — added \`tsc --noEmit\`. Renamed two unused function params and widened a function's first arg from \`AsyncGenerator<StreamChunk>\` to \`AsyncIterable<StreamChunk>\` to match \`chat()\`'s return type. Same anti-pattern (spread of a per-provider \`chat()\` union erasing the generic) is present in \`ts-react-chat\` but doesn't surface under its current tsconfig — left in place to avoid scope creep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: drop check-ts-suppressions.mjs (redundant with ban-ts-comment) The script was introduced in 0714eea as a backstop for `@ts-ignore` / `@ts-nocheck` enforcement. Its value-add at the time was that the typed-linting block in `eslint.config.js` (which configures `@typescript-eslint/ban-ts-comment` with `'ts-ignore': false` / `'ts-nocheck': true`) silently didn't apply to four packages — `ai`, `ai-client`, `ai-svelte`, `ai-fal` — because each shipped a no-op local `eslint.config.js` that shifted the `files` glob's resolution context. Without the script, those four packages were under the base config's looser policy (allow `@ts-ignore` with a description). The previous commit (720ffd6) deletes those four local configs, so the strict `ban-ts-comment` policy now applies uniformly. The script and ESLint enforce identical rules against identical scopes, both gated by the same CI step. Keeping both creates divergence risk (regex vs. AST policies that can drift), so consolidate on ESLint. Removes: - `scripts/check-ts-suppressions.mjs` - `test:no-suppressions` script in root `package.json` - The `pnpm run test:no-suppressions && ` prefix on `test:pr` and `test:ci` — `test:eslint` already runs in both and catches the same violations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(rebase): absorb main drift after rebase onto 144e090 - Anthropic per-model type-test: switch to `id`-form aliases (`claude-3-5-haiku` / `claude-3-haiku`) after #581 narrowed the factory's accepted model union to `id` values rather than `name`. - Re-add `as unknown as UseChatReturn<TTools, TSchema>` cast on the React `useChat` return — main's new `activeStructuredPart` memo changed the runtime shape, so the conditional return-type seam needs the double-cast (the no-restricted-syntax eslint-disable was already in place). - Bracket-notation fixes for `noPropertyAccessFromIndexSignature` violations newly surfaced in code that landed on main: - `process.env.X` reads in gemini/grok test files - `process.env.NODE_ENV` in ai-devtools/src/index.ts - `e.name` / `e.message` index-sig reads in ai-isolate-cloudflare/src/isolate-driver.ts - `Record<string, unknown>` access in message-updaters / openai-base / elevenlabs tests - Cast schema-converter test results to `any` (`const result: any = ...`) so dot-chains on the returned JSON Schema satisfy the new flag without 50+ bracket rewrites; typed-lint rules don't apply to test files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(types): enable useDefineForClassFields Flips the fifth strict flag from issue #564's original five-flag set on. With ES2020 target, `useDefineForClassFields` is off by default; turning it on emits real ES2022 class fields (Object.defineProperty) instead of [[Set]]-style assignments — closer to runtime semantics and required to keep TS in sync with downstream consumers that default to true (ES2022+ targets). Surfaced one test breakage in `ai-openrouter/tests/openrouter-adapter.test.ts`: the inline `vi.mock` factory returned a class with a `chat` field initializer, and vitest's mock-hoister mis-rewrote that field reference because `chat` was also a top-level named import on line 2. The class-field semantics under `useDefineForClassFields: true` turned the previously-silent collision into a runtime "Cannot access '__vi_import_0__' before initialization" error. Rewrite the mock as a plain constructor function with `this.chat = ...` assignments. The sibling `openrouter-responses-adapter.test.ts` test uses the same inline-class pattern but doesn't collide (field is named `beta`, no matching import) so it's left as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(types): ratchet warn-level lint baselines in ai-fal and ai-preact The typed-ESLint block leaves `no-explicit-any`, `no-non-null-assertion`, and `prefer-readonly` at `warn` to ratchet later. Clears the warnings in the two packages explicitly called out for cleanup; the remaining ~600 warnings in other packages are a separate sweep. ai-fal: - `utils/client.ts:41` — `url.split('?')[0]!.split('#')[0]!` had two forbidden non-null assertions guarding `noUncheckedIndexedAccess`. Both `String.split` calls return a non-empty array when the separator is missing, but TS doesn't know that. Replace with `?.split('#')[0] ?? url` — falls back to the raw input string if the optional chain ever produces undefined. - `utils/client.ts:137` — `view[i]!` inside an indexed `for` loop. Rewrite as `for (const byte of view)` so the binding is statically non-undefined. - `model-meta.ts:26,128` — fallback branches for the `EndpointTypeMap[TModel]` conditional types used `Record<string, any>`. Tighten to `Record<string, unknown>` so consumers get a value type they have to narrow. - `adapters/video.ts:140` — `catch (error: any)` then `error?.body`, `error.message`. Narrow the catch binding to `unknown` and introduce a typed local alias for the SDK error shape we read. ai-preact: - `addToolResult.output: any` → `output: unknown` in both `types.ts` and `use-chat.ts`. The framework genuinely doesn't know the result shape — `unknown` forces callers to narrow rather than silently accepting anything assignable to `any`. The `<TTools extends ReadonlyArray<AnyClientTool> = any>` defaults on the hook generics are deliberately kept — that pattern is shared by ai-react, ai-solid, ai-svelte, ai-vue, and ai-preact. Changing the default in one framework only would diverge the surface, and changing it everywhere is its own refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(types): enable exactOptionalPropertyTypes Flips the final flag from issue #564's original five-flag set on, and fixes the ~300 errors it surfaces across 26 packages. EOPT distinguishes `field?: T` (the property may be absent) from `field?: T | undefined` (may be absent OR explicitly undefined), so spreading an object whose source field is `T | undefined` into a target declared `field?: T` is now a type error. ## Fix patterns Two patterns covered nearly every site. The choice between them is about semantic intent, not convenience. **Widen the target** (`field?: T` → `field?: T | undefined`): used when the field is locally owned and the carry-through-undefined semantics are correct. Most internal interfaces fall here — `BaseEventContext`-style shared-context types, middleware context shapes, devtools store interfaces, eval-local row types, etc. **Conditional spread** (`{ ...(v !== undefined && { field: v }) }`): used when the target is owned by another package (vendor SDK, AG-UI event shapes, framework integration options imported from `@tanstack/ai-client`, etc.) and absence-on-the-wire matters. Provider adapters (OpenAI, Anthropic, Gemini, Ollama, Grok, fal, ElevenLabs, OpenRouter, Groq) all use this for vendor request shapes; framework hooks (ai-react, ai-solid, ai-vue, ai-svelte, ai-preact) use it for `ChatClientOptions`/`GenerationClientOptions` pass-throughs. ## Things deliberately NOT widened - `Tool.inputSchema` / `Tool.outputSchema` and the same fields on `ClientTool`. These participate in `InferToolInput` / `InferToolOutput` via `infer T extends StandardJSONSchemaV1<…>`. Widening to `T | undefined` makes the inference distribute over `undefined` and collapses inferred input/output types to `unknown`, breaking the `chat()` / `useChat()` type-inference contract that the public type-tests in `ai-client/tests/` lock down. Comment added at the declaration to flag this. - Mirror types in `ai-event-client/src/index.ts` (the locally redeclared `ContentPartUrlSource`, `ToolCallPart`, `ToolResultPart`, `ToolCall`, `ImageUsage`) — these must structurally match the authoritative declarations in `@tanstack/ai/src/types.ts` to stay cross-package assignable. Kept as `field?: T` (no `| undefined`). ## Notable per-package work - `ai` (71 errors): widened the shared context / middleware / activity option types. One `as ChatMiddleware` boundary cast on the devtools-middleware structural-equivalent type (sibling package, deliberately duplicated to avoid circular dep). Two conditional spreads at AG-UI `RUN_ERROR` / image-output wire boundaries. - `ai-event-client` (21 errors): widened `BaseEventContext` and the per-event subtype redeclarations of optional fields. Mirror types for content parts deliberately not widened (see above). - `ai-devtools` (31 errors): widened the five store interfaces in `store/ai-context.tsx` (`MessagePart`, `Message`, `Chunk`, `Iteration`, `Conversation`); removed two now-unnecessary `as T[keyof T]` casts. - `openai-base`, `ai-openai`, `ai-openrouter`, `ai-grok`, `ai-groq`: conditional-spread the `RUN_ERROR.code` / nested `error.code` and `RUN_FINISHED.usage` wire payloads, plus `signal` and `headers` into vendor `RequestOptions`. - `ai-anthropic`, `ai-gemini`: conditional-spread vendor-SDK request shape fields (`ThinkingConfig`, `WebSearchTool20250305`, etc). `brandProviderTool` tool factories pass `metadata: config` directly — `Tool.metadata` was widened to allow `| undefined` so the property stays present (smoke tests assert `toHaveProperty('metadata')`). - `ai-isolate-{node,quickjs,cloudflare}`: conditional-spread `NormalizedError.stack` / `code` (type owned by `ai-code-mode`, intentionally narrow). Local `ExecuteResponse` widened. - Framework hooks (`ai-react`, `ai-solid`, `ai-vue`, `ai-svelte`, `ai-preact`): conditional-spread every optional pass-through to `ChatClient` / `GenerationClient` / `VideoGenerationClient` constructors and `updateOptions` calls. Block-bodied callback wrappers in `ai-react` so the `?.()` chain doesn't widen the callback's return type to `void | undefined`. - `ai-openrouter`: vitest mock-hoister collision under `useDefineForClassFields: true` (from the prior commit) needed the inline-class mock rewritten as a constructor function — same fix logic as the `chat = {…}` collision noted there. ## Verification - `pnpm test:types`: 32 projects, 0 errors. - `pnpm test:eslint`: 31 projects, 0 errors (warning baselines unchanged). - `pnpm test:lib`: 31 projects, all tests pass. - `pnpm build`: 31 projects. - 9 examples: all `test:types` exit 0 (one example, `ts-react-media`, needed a local `JobState` discriminant widening + conditional spread on a `RequestInit`-style fetch payload). Covers issue #564 (Stage 3 — final flag). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * Fix changest * refactor(types): back out two strict TS flags, tighten lint rules Drop noPropertyAccessFromIndexSignature and exactOptionalPropertyTypes from tsconfig.base.json — both produced ~500 lines of bracket-access / conditional-spread churn without catching real bugs, and EOPT would have propagated style preferences to downstream consumers running the same flag. Two surgical fix-ups restore narrowing the EOPT-removed explicit guards relied on (realtime-client status/mode dispatch; gemini functionResponse optional chain). Re-enable @typescript-eslint/no-non-null-assertion as an error and refactor all 63 sites across 29 files to avoid `!`: - Map get-then-has → bind result to local, init if missing - array[i] in known-bounded loops → for-of / .entries() / optional chain - arr.shift()! in while → assignment-in-condition pattern - closure-captured non-null → capture into const outside the closure (TS can't propagate narrowing through arrow boundaries) - vendor SDK "known present" fields → narrowing type predicates on .filter, destructure-with-undefined-check, or guarded if-continues Upgrade @typescript-eslint/prefer-readonly from warn to error and fix the 18 sites it surfaced. Keep @typescript-eslint/no-explicit-any as a warning per maintainer preference — existing ~165 warnings unchanged; new introductions surface in editors without blocking CI. Reduce eslint-disable count from 25 to 23; every remaining disable now carries an inline reason. File-level disable in schema-converter .ts replaced with two targeted line-level disables. Dead \`part.type === 'document'\` branch removed from ai-context.tsx. \`while (true)\` in SSE/connection adapters rewritten as \`while (!abortSignal?.aborted)\`. The require-await warning on the sync in-memory SkillStorage implementation refactored to \`Promise.resolve()\` wrappers (no async, no disable). Update changeset to reflect the final rule set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(types): infer AudioProviderOptions from ~types brand Switch AudioProviderOptions from a nominal `AudioAdapter<any, any>` match to a structural `{ '~types': { providerOptions: infer P extends object } }` extraction. The previous conditional silently collapsed to `object` for concrete provider options interfaces without an index signature (the common case), leaving `modelOptions` effectively untyped at activity call sites. The new form carries the real per-model provider options through, restores model-specific typing on `modelOptions`, and removes the last `any` from the audio extraction helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 308468d commit 2ad137b

204 files changed

Lines changed: 3416 additions & 1171 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@tanstack/ai': patch
3+
'@tanstack/ai-anthropic': patch
4+
'@tanstack/ai-code-mode': patch
5+
'@tanstack/ai-code-mode-skills': patch
6+
'@tanstack/ai-devtools-core': patch
7+
'@tanstack/ai-elevenlabs': patch
8+
'@tanstack/ai-fal': patch
9+
'@tanstack/ai-gemini': patch
10+
'@tanstack/ai-grok': patch
11+
'@tanstack/ai-groq': patch
12+
'@tanstack/ai-isolate-node': patch
13+
'@tanstack/ai-isolate-quickjs': patch
14+
'@tanstack/ai-ollama': patch
15+
'@tanstack/ai-openai': patch
16+
'@tanstack/ai-openrouter': patch
17+
'@tanstack/ai-react-ui': patch
18+
'@tanstack/ai-solid-ui': patch
19+
'@tanstack/ai-vue-ui': patch
20+
'@tanstack/openai-base': patch
21+
'@tanstack/preact-ai-devtools': patch
22+
'@tanstack/react-ai-devtools': patch
23+
'@tanstack/solid-ai-devtools': patch
24+
---
25+
26+
Tighten TypeScript safety: enable `noImplicitOverride`,
27+
`noFallthroughCasesInSwitch`, and `useDefineForClassFields` in the
28+
root `tsconfig.json`; add a typed-ESLint block scoped to
29+
`packages/typescript/*/src/**` that turns on `no-floating-promises`,
30+
`no-misused-promises`, `await-thenable`,
31+
`switch-exhaustiveness-check`, `consistent-type-exports`,
32+
`prefer-readonly`, and `no-non-null-assertion` (errors), plus
33+
`no-explicit-any` (warning). `@ts-ignore` and `@ts-nocheck` are
34+
disallowed in library source via `@typescript-eslint/ban-ts-comment`,
35+
and `as unknown as <T>` double-casts are blocked by a
36+
`no-restricted-syntax` rule (escape hatches available with an inline
37+
reason). Two flags from the original five-flag set —
38+
`noPropertyAccessFromIndexSignature` and `exactOptionalPropertyTypes`
39+
— were tried and rolled back: they produced ~500 lines of bracket-
40+
access and conditional-spread churn without catching any real bugs,
41+
and `exactOptionalPropertyTypes` would have forced consumers using
42+
it themselves to deal with our internals' style preferences.
43+
44+
User-visible API surface is unchanged; this is a hardening pass to
45+
keep streaming/agent-loop correctness and discriminated-union
46+
exhaustiveness honest going forward. See issue #564.

eslint.config.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,74 @@ const config = [
1919
'pnpm/json-enforce-catalog': 'off',
2020
},
2121
},
22+
{
23+
// Typed-linting rules scoped to library source — issue #564.
24+
//
25+
// Restricted to `packages/typescript/*/src/**` so streaming + agent-loop
26+
// bugs that violate `no-floating-promises`, exhaustive-switch checks, or
27+
// async-misuse guarantees fail in CI without dragging tests, examples,
28+
// or build artefacts under the typed-linting cost.
29+
name: 'tanstack/ai/typed',
30+
files: ['packages/typescript/*/src/**/*.{ts,tsx}'],
31+
rules: {
32+
'@typescript-eslint/no-floating-promises': 'error',
33+
'@typescript-eslint/no-misused-promises': 'error',
34+
'@typescript-eslint/await-thenable': 'error',
35+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
36+
'@typescript-eslint/consistent-type-exports': 'error',
37+
'@typescript-eslint/prefer-readonly': 'error',
38+
'@typescript-eslint/no-non-null-assertion': 'error',
39+
// `no-explicit-any` stays as a warning — much of the existing `any`
40+
// is structurally load-bearing (`Tool<any, any>` / `Adapter<any>`
41+
// variance wildcards, `Record<string, any>` provider option
42+
// carriers), but new `any` introductions should still get a second
43+
// look. Tracked as warnings to surface in editors without blocking CI.
44+
'@typescript-eslint/no-explicit-any': 'warn',
45+
// Override the base config which currently allows `@ts-ignore` with a
46+
// description and forbids `@ts-expect-error`. Invert that: require
47+
// descriptions on `@ts-expect-error` (which self-heals when the
48+
// underlying error disappears) and disallow `@ts-ignore` outright.
49+
'@typescript-eslint/ban-ts-comment': [
50+
'error',
51+
{
52+
'ts-expect-error': 'allow-with-description',
53+
'ts-ignore': false,
54+
'ts-nocheck': true,
55+
'ts-check': false,
56+
},
57+
],
58+
},
59+
},
60+
{
61+
// `no-restricted-syntax` banning `as unknown as <Type>` double-casts.
62+
//
63+
// Why separate from the typed-linting block: a few packages still ship a
64+
// local `eslint.config.js` that re-exports root. Flat-config evaluates
65+
// `files` globs relative to the *config-file's* directory, so the typed
66+
// block's `packages/typescript/*/src/**` glob fails to match anything
67+
// when re-exported from inside `packages/typescript/<pkg>/`. The dual
68+
// glob below works in both contexts.
69+
//
70+
// Why ban `as unknown as T`: it bypasses TS's structural-overlap check
71+
// (the safety net that errors when two types don't sufficiently
72+
// overlap), like `@ts-ignore` for type assertions. Plain `as T` keeps
73+
// the check. Genuine boundaries (vendor SDK shape drift, DOM lib
74+
// limitations, conditional-return narrowing failures) can opt out via
75+
// `// eslint-disable-next-line no-restricted-syntax -- <reason>`.
76+
name: 'tanstack/ai/no-double-as',
77+
files: ['packages/typescript/*/src/**/*.{ts,tsx}', 'src/**/*.{ts,tsx}'],
78+
rules: {
79+
'no-restricted-syntax': [
80+
'error',
81+
{
82+
selector:
83+
"TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
84+
message:
85+
"Avoid `as unknown as <Type>` — it bypasses TS's structural overlap check. Prefer plain `as <Type>`, fix the root cause, or opt out with `// eslint-disable-next-line no-restricted-syntax -- <reason>`.",
86+
},
87+
],
88+
},
89+
},
2290
]
2391

2492
export default config

examples/ts-code-mode-web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "vite dev --port 3001",
77
"build": "vite build",
88
"serve": "vite preview",
9-
"test": "exit 0"
9+
"test": "exit 0",
10+
"test:types": "tsc --noEmit"
1011
},
1112
"dependencies": {
1213
"@radix-ui/react-collapsible": "^1.1.2",

examples/ts-code-mode-web/src/lib/reports/create-handler-bindings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ function createBinding(
7474

7575
export function createHandlerBindings(
7676
context?: HandlerBindingContext,
77-
reportId?: string,
78-
canvasId: string = 'diagram',
77+
_reportId?: string,
78+
_canvasId: string = 'diagram',
7979
): Record<string, ToolBinding> {
8080
// Helper to track binding calls
8181
const track = (name: string) => context?.onBindingCall?.(name)

examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ function instrumentAdapter(adapter: AnyTextAdapter): {
188188
}
189189

190190
function wrapWithTimingEvents(
191-
stream: AsyncGenerator<StreamChunk>,
191+
stream: AsyncIterable<StreamChunk>,
192192
adapter: AnyTextAdapter,
193193
): AsyncGenerator<StreamChunk> {
194194
const requestStartTimeMs = Date.now()

examples/ts-group-chat/chat-server/capnweb-rpc.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ export const clients = new Map<string, (...args: Array<any>) => void>()
6363
// Chat Server Implementation (one per connection)
6464
export class ChatServer extends RpcTarget {
6565
public currentUsername: string | null = null
66-
private webSocket: WebSocket | null = null
6766

6867
constructor() {
6968
super()
@@ -74,8 +73,6 @@ export class ChatServer extends RpcTarget {
7473

7574
// Set the WebSocket connection for this server instance
7675
setWebSocket(ws: WebSocket) {
77-
this.webSocket = ws
78-
7976
// Handle WebSocket disconnection
8077
ws.on('close', () => {
8178
if (this.currentUsername) {

examples/ts-group-chat/chat-server/claude-service.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ const getWeatherTool = toolDefinition({
4040
'Get the current weather for a location. Returns temperature, conditions, and humidity.',
4141
inputSchema: getWeatherInputSchema,
4242
outputSchema: getWeatherOutputSchema,
43-
}).server((args) => {
43+
}).server((rawArgs) => {
44+
// JSONSchema doesn't carry compile-time types, so `rawArgs` is `unknown`.
45+
const args = rawArgs as { location: string; unit?: string }
4446
// Mock weather data - in a real app this would call a weather API
4547
const mockWeatherData: Record<
4648
string,
@@ -53,8 +55,8 @@ const getWeatherTool = toolDefinition({
5355
sydney: { temp: 78, conditions: 'Clear skies', humidity: 45 },
5456
}
5557

56-
const location = (args.location as string).toLowerCase()
57-
const unit = (args.unit as string | undefined) ?? 'celsius'
58+
const location = args.location.toLowerCase()
59+
const unit = args.unit ?? 'celsius'
5860
const weather = mockWeatherData[location] ?? {
5961
temp: 65,
6062
conditions: 'Unknown',
@@ -157,7 +159,7 @@ export class ClaudeService {
157159
})) {
158160
chunkCount++
159161

160-
if (chunk.type === 'content' && chunk.delta) {
162+
if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
161163
accumulatedContent += chunk.delta
162164
console.log(
163165
`🤖 Claude: Chunk #${chunkCount} [content] delta: "${chunk.delta}" (total: ${accumulatedContent.length} chars)`,

examples/ts-group-chat/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"scripts": {
66
"dev": "vite dev --port 4000",
77
"build": "exit 0",
8-
"test": "exit 0"
8+
"test": "exit 0",
9+
"test:types": "tsc --noEmit"
910
},
1011
"dependencies": {
1112
"@tailwindcss/vite": "^4.1.18",

examples/ts-group-chat/src/routes/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createFileRoute, Link } from '@tanstack/react-router'
1+
import { createFileRoute } from '@tanstack/react-router'
22
import { useState, useEffect } from 'react'
33

44
import { useChatConnection } from '@/hooks/useChatConnection'

examples/ts-react-chat/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "vite dev --port 3000",
77
"build": "vite build",
88
"serve": "vite preview",
9-
"test": "exit 0"
9+
"test": "exit 0",
10+
"test:types": "tsc --noEmit"
1011
},
1112
"dependencies": {
1213
"@tailwindcss/vite": "^4.1.18",

0 commit comments

Comments
 (0)