Skip to content

feat(components): compile under noImplicitAny — type the 26 renderer signatures + 2 test sites - #4417

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4353-components-no-implicit-any
Aug 12, 2026
Merged

feat(components): compile under noImplicitAny — type the 26 renderer signatures + 2 test sites#4417
yinlianghui merged 1 commit into
mainfrom
claude/issue-4353-components-no-implicit-any

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4353

@object-ui/components was the only package in the workspace relaxing a strict sub-flag. This turns noImplicitAny on and gives real types to every signature that depended on it being off. Types only — zero runtime change; all 1077 of the package's tests pass untouched.

The stale comment above the flag (it explained the adjacent rootDir removal, not the flag) is gone with the flag. tsconfig.test.json's deliberate mirror of the same flag is removed too, and its inline reasoning rewritten rather than deleted: that mirror existed so a TEST project could not become the compiler of record for a SOURCE strictness decision tsconfig.json owns. With the build config no longer relaxing the flag, a mirror would invert exactly that hazard — the test project would be relaxing what the source config tightened. Both projects now simply inherit strict: true from the root config.

Pre-fix measurement — the red

Baseline on origin/main @ 0b49d6032, both commands of pnpm type-check: 0 errors. Removing the flag from both configs, unchanged sources:

file sites code
src/renderers/navigation/sidebar.tsx 10 TS7031 (9 schema + 1 className binding elements)
src/renderers/action/action-bar.tsx 6 TS7006
src/renderers/action/action-menu.tsx 4 TS7006
src/renderers/action/action-group.tsx 4 TS7006
src/renderers/data-display/tree-view.tsx 2 TS7031
source total 26
src/__tests__/page-header-predicate-dialect.test.tsx 1 TS7006
src/__tests__/div-deprecation-warn-once.test.tsx 1 TS7006
total 28

This reproduces the card's table exactly — same files, same counts, main movement notwithstanding. No site in data-table.tsx, so nothing here overlaps #4354; that file is untouched by this PR.

Measured convention — the sidebar entry points

The card left open whether the ten { schema } entry points want a real schema type or ComponentRendererProps. Measured across the renderer tree:

  • 21 occurrences of an inline annotation naming the registered component's own schema type: ({ schema, ...props }: { schema: BadgeSchema; [key: string]: any }), with className?: string spelled out when className is destructured (10 files do exactly that).
  • 0 occurrences of ComponentRendererProps in this package — it exists in both @object-ui/core and @object-ui/types, and nothing here uses it.
  • 8 occurrences of React.FC< any > on named renderer components in containers.tsx, which types nothing and is not a model worth copying.

So: the inline per-schema-type spelling, which sidebar.tsx line 49 already uses for its one typed registration. No third spelling invented.

Which schema type, though. Only 'sidebar' itself is in the registry map (@object-ui/types, registry.ts). The other ten registrations are sidebar partssidebar-header, sidebar-menu-button, … — and have no schema type of their own. They take BaseSchema, the type every registered node satisfies and the one that actually declares the body / label keys they read. Annotating them SidebarSchema would assert type: 'sidebar' on a node whose type is 'sidebar-header', which is simply false and would make an honest schema.type === 'sidebar-header' test a compile error later.

The action callbacks, and what typing them exposed

Typing these surfaced a real defect that the flag had been hiding, so this section is longer than the card anticipated.

Why they were untyped at all. Not an oversight at the callback. All three renderers are forwardRef components whose props type carries [key: string]: any. forwardRef routes its props through PropsWithoutRef, which is "ref" extends keyof Props ? Omit< Props, "ref" > : Props. An index signature puts string in keyof Props, so the first branch always wins, and Omit over a type with a string index signature collapses every declared property into the bare index signature. schema therefore arrived as any — proved in situ, not inferred: an added const probe: null = schema; raised no error, which only any does under strictNullChecks. Every filter / some / map callback below it inherited that any.

So the fix annotates each action list once, where it enters, and the chains below infer. That is 2 annotations in action-bar.tsx covering its 6 sites, 1 in action-group.tsx covering 4, and 1 in action-menu.tsx covering 4.

Which action type. The card says "typed from ActionDef — the type they receive at runtime". Annotating with the ActionSchema these files already import went red in four places, and every one of them says the same thing: that import is the wrong type. @object-ui/types exports two action types — ActionSchema (from crud.ts, carrying its own @deprecated Use UIActionSchema for new code) and UIActionSchema (from ui-action.ts, the modern one). These renderers import the legacy one for their declarations but are written against the modern one:

  • actionRendersAt, the shared placement predicate, takes { locations?: readonly string[] }. Legacy ActionSchema has no locations — TS2559, weak-type detection, in both action-bar and action-group.
  • The objectui#2339 ordering tie-break compares a.variant === 'primary'. Legacy variant is 'default' | 'outline' | 'ghost' | 'link' — TS2367, three times. action-group and action-menu already carry (action.variant as string) casts written to get around exactly this.
  • Legacy type is the literal 'action'. The actions flowing through these renderers carry 'form' | 'script' | 'url' | 'flow' | 'api' | 'modal' — TS2322 at every leaf handoff. action-bar's own documented example at the top of the file is a UIActionSchema (type: 'script').

The callbacks are therefore typed from UIActionSchema, and the internal wiring that carries those values — the combinedOverflow memo's type argument, the leaf components' action / onExecute / onSelect props, the two handleExecute callbacks — moves with them, because a value cannot be one type in the list and another in the leaf.

What deliberately did NOT move: the actions?: ActionSchema[] keys on ActionBarSchema / ActionMenuSchema / ActionGroupSchema. Reconciling those declarations with the type the implementation receives reaches ~46 references across 12 files and is a contract decision in its own right, not part of turning a compiler flag on. Filed separately (see below); the annotations name the mismatch in prose at each site so the next reader is not left guessing.

Per-file typing

file sites how
sidebar.tsx 10 inline per-schema-type annotation; BaseSchema for the ten parts, SidebarSchema untouched on 'sidebar'
tree-view.tsx 2 one annotation, TreeViewSchema — already imported, and previously flagged unused
action-bar.tsx 6 2 annotations at the two list entry points; rest inferred
action-group.tsx 4 1 annotation at the list entry point; rest inferred
action-menu.tsx 4 1 annotation at the list entry point; rest inferred
2 test files 2 unknown[], the row type deprecationCalls already declares it returns. ReturnType< typeof vi.spyOn > erases the spied signature, so mock.calls arrives as any and the parameters had nothing to infer from

Verification

  • pnpm exec tsc --noEmit and tsc -p tsconfig.test.json, flag ON: both exit 0. (Both were exit 2 with 26 / 28 errors before the typings.)
  • Repo-root vitest, pnpm exec vitest run packages/components/ --maxWorkers=2: 121 files, 1077 tests, all passed, exit 0. No test added or removed, so the counts are structurally unchanged.
  • eslint on the 7 touched source files: 0 errors (46 warnings — see below).
  • node scripts/check-control-bytes.mjs: OK, 4125 files scanned. Plus a direct control-byte scan of the 10 changed files: clean.
  • node scripts/check-phantom-dependencies.mjs: green.
  • node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs: green.
  • No downstream consumer sweep was run, deliberately. Nothing this package publishes changes shape: src/index.ts re-exports none of the changed symbols — not the three action schema interfaces, not InlineActionButton / DropdownActionItem / ActionMenuItem, not the registered renderers (which are side-effect registrations). The barrels it does export from — ./ui, ./custom, ./notifications, ./debug, ./share — contain none of them. With no exported declaration moving, a downstream sweep has nothing to detect.

Reverse verification

Direction predicted before running, and it is the plain red one: noImplicitAny judges each binding independently, so there are no counts to move and no predicate to invert. Method was commit-then-revert via git checkout origin/main -- FILE (never git stash).

  • Revert sidebar.tsx alone, flag still on → predicted 10 × TS7031 at lines 29, 74, 87, 100, 124, 138, 151, 174, 187, 200 and nothing elsewhere. Got exactly that, exit 2.
  • Revert action-menu.tsx alone → predicted 4 × TS7006 at 278,23 / 278,31 / 315,27 / 315,35. Got exactly that, exit 2.

Both restored; the flag is now load-bearing rather than decorative.

Lint delta — measured, and it goes the other way

The dispatch expected the no-explicit-any warning count to drop. It rises: 31 to 42 (+11), and reporting that honestly matters more than the expectation.

Every one of the +11 is a [key: string]: any index signature in a newly added annotation — 10 in sidebar.tsx, 1 in tree-view.tsx — because that index signature is part of the measured convention, which exists so the registry can spread arbitrary props onto the underlying Shadcn component. The action files add zero (4 / 14 / 10, all unchanged): typing a list at its entry point introduces no any at all. Spelling the index signature unknown instead would break every {...props} spread and would be the third spelling the ruling forbids.

So the trade is exact and worth naming: 26 implicit anys — invisible, unbounded, and silently propagating into every downstream inference — become 11 explicit, bounded ones confined to a props index signature, while every schema and every action callback gains a real type. Total warnings 36 to 46, 0 errors either way.

One warning also disappeared: 'TreeViewSchema' is defined but never used. The type was imported and dead; typing the renderer put it to work.

Changeset

patch, per the card's rule for pure inference-tightening with unchanged exported declarations — and the published surface is verifiably unchanged, as set out under Verification. The #4403 minor precedent does not apply because no exported declaration visibly changes shape.

Out of scope, filed separately

  • The legacy-vs-modern action type mismatch described above (the actions?: ActionSchema[] declarations vs the UIActionSchema the implementation receives).
  • The PropsWithoutRef props-collapse trap, which silently erases declared prop types from any forwardRef component whose props type carries an index signature — 11 files in this package alone.

Generated by Claude Code

`packages/components/tsconfig.json` was the only place in the workspace
relaxing a `strict` sub-flag, under a comment that explained the adjacent
`rootDir` removal rather than the flag. `tsconfig.test.json` mirrored the
one flag deliberately, so the test project could not become the compiler
of record for a source strictness decision the build config owns.

Both configs now inherit `strict: true` from the root. The flag flip
reported 26 implicitly-`any` sites in five renderer sources and 2 in the
package's own tests; all 28 are typed. Types only — no runtime change.

The sidebar entry points follow the package's measured convention (an
inline `{ schema: <X>Schema; [key: string]: any }` naming the registered
component's schema type, 21 occurrences). The ten sidebar PARTS have no
schema type of their own and take `BaseSchema`; `SidebarSchema` would
assert `type: 'sidebar'` on a `'sidebar-header'` node.

The action callbacks are typed from `UIActionSchema`, not the legacy
`ActionSchema` these files import for their declarations: the legacy
interface has no `locations` (which `actionRendersAt` requires), no
`'primary'` variant (which the #2339 tie-break compares against), and a
literal `type: 'action'` where the actions flowing through carry
`'form' | 'script' | 'url' | 'flow' | 'api' | 'modal'`. That was
unverifiable before, because `forwardRef` routes props through
`PropsWithoutRef`, whose `Omit` collapses a props type carrying
`[key: string]: any` to the bare index signature — so `schema` arrived
as `any` and every callback under it inferred `any`.

Nothing this package publishes changes shape: the three action schema
interfaces and the leaf components are not re-exported from `src/index.ts`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 12, 2026 5:19am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-BCq74OTP.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.20KB 108.43KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.42KB 41.19KB
fields (index.js) 228.69KB 56.74KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.01KB 17.63KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 120.57KB 31.32KB
plugin-designer (index.js) 211.16KB 42.76KB
plugin-detail (index.js) 239.03KB 59.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.99KB 49.92KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.21KB 26.79KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.99KB 10.74KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

ACCEPT — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #4353.

  • The convention was measured and followed, including its uncomfortable part: the 21-occurrence inline spelling carries a [key: string]: any index signature, so the lint delta goes UP by 11 explicit bounded anys while 26 invisible propagating ones die — reported as the net trade it is rather than forced to match my dispatch's expectation. The BaseSchema-for-parts distinction (a SidebarSchema annotation would falsely assert type: 'sidebar' on a header node) is exactly the measure-don't-assume standard.
  • The action-typing scope growth is compiler-proven necessity, not creep: four independent proofs that the imported legacy ActionSchema is the wrong type (locations, variant, type literals, the file's own example), and the internal wiring moving with the callbacks because a value cannot be one type in the list and another in the leaf. Stopping at the exported actions?: ActionSchema[] keys and filing finding(components): the action renderers declare the deprecated ActionSchema but are written against UIActionSchema #4418 (~46 refs, a contract decision) is the right mega-diff refusal.
  • The RV set's exact-line predictions plus the in-situ const probe: null root-cause proof — which surfaced finding(components): forwardRef + a props index signature silently erases every declared prop type — 11 renderers affected #4422 (forwardRef + index signature erasing prop types, a hole this flag structurally cannot see) — turn a strictness card into two well-measured follow-ups.
  • The test-config divergence note (a mirror would now RELAX what the source config tightened — the original hazard inverted) is correctly reasoned and recorded in place. Changeset patch verified against the barrel. CI converged.

Flipping ready + arming auto-merge.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 12, 2026 05:32
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 4dadf0d Aug 12, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4353-components-no-implicit-any branch August 12, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(components): noImplicitAny is off package-wide, and 26 renderer signatures depend on it

2 participants