Skip to content

fix(types): stop advertising isAction on components that cannot honour it - #517

Merged
IgorShevchik merged 2 commits into
mainfrom
fix/is-action-not-inherited
Aug 30, 2026
Merged

fix(types): stop advertising isAction on components that cannot honour it#517
IgorShevchik merged 2 commits into
mainfrom
fix/is-action-not-inherited

Conversation

@IgorShevchik

@IgorShevchik IgorShevchik commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Linked issue

None — found while re-measuring #88 against upstream.

Type of change

  • Documentation (updates to the documentation or readme)
  • Bug fix (a non-breaking change that fixes an issue)
  • Enhancement (improving an existing functionality)
  • New feature (a non-breaking change that adds functionality)
  • Chore (updates to the build process or auxiliary tools and libraries)
  • Revert (undoing a merged change — retitle this PR revert(Scope): ...)
  • Breaking change (fix or feature that would cause existing functionality to change)

⚠️ Shipping as a patch, deliberately — but it can fail a downstream build

Runtime behaviour does not change: isAction never reached anything outside Link, so nothing that worked before stops working. What changes is that TypeScript now rejects isAction where it used to accept it silently. A project passing it to B24Button or to a page/footer/content-search link will fail to compile after upgrading.

That was weighed: a BREAKING CHANGE footer would take the package to a major version for the removal of a prop that never did anything, and a silent patch would hand people a red build with no warning. The middle course is this — fix(types) with the consequence stated plainly here and in the squash body, so it reads as a visible line in the changelog rather than a surprise. The fix is a one-word deletion at the call site.

Description

isAction restyles a link as Bitrix24's dashed "action" text. src/theme/link.ts is the only theme that implements it, and it is deliberately absent from linkKeys — so pickLinkProps never forwards it downstream.

Everything spreading LinkProps inherited it anyway. A consumer could write isAction on a B24Button or a breadcrumb item, TypeScript accepted it, Vue registered it, and nothing changed on screen. Button declared it as a real runtime prop:

before   Object.keys(Button.props).length === 45   // includes 'isAction'
after    Object.keys(Button.props).length === 44

Ten types across nine files omit it now — ButtonProps, BreadcrumbItem, CommandPaletteItem, ContentSearchItem, ContentSearchLink, ContextMenuItem, DropdownMenuItem, FooterColumnLink, NavigationMenuItem, PageLink — and anything extending ButtonProps follows. Link keeps it.

Nothing in the repository passed it anywhere but B24Link, so no example, playground or documentation changes.

What is enforceable, and what is not

The omission does not mean the same thing everywhere, and the spec says so rather than pretending:

type index signature omission enforceable
ButtonProps no yes
PageLink no yes
FooterColumnLink no yes
ContentSearchLink no yes
BreadcrumbItem, DropdownMenuItem, NavigationMenuItem, ContextMenuItem, CommandPaletteItem, ContentSearchItem [key: string]: any no

Those six accept arbitrary attributes by design, so keyof includes string and no Omit can make isAction unassignable on them. Their omission states intent the compiler cannot check. The spec asserts only the four it can — a test that cannot fail is worse than none.

The guard, and why it is in two halves

They fail under different commands, and neither covers the other:

  • expect(Object.keys(Button.props)).not.toContain('isAction') and expect(linkKeys).not.toContain('isAction') fail under vitest run.
  • The expectTypeOf assertions are erased at runtime; only vue-tsc (pnpm run typecheck) checks them.

Both mutation-checked: reverting Button's omit reddens two tests in vitest run; reverting PageLink's or ContentSearchLink's reddens typecheck.

The type predicate is written out as type HasIsAction<T> = 'isAction' extends keyof T ? true : false rather than using expectTypeOf().not.toHaveProperty(). The latter was tried first and gave results that did not match how it reads on types with an index signature, with error TS2554: Expected 2 arguments, but got 1 as its failure message.

Review

/review caught the first sweep reading only src/runtime/components/*.vue and never descending into content/, leaving ContentSearchLink and ContentSearchItem still advertising the prop. Fixed in the second commit; the sweep is now a search across all of src/, and ContentSearchLink is mutation-checked with the rest.

The five-reviewer panel was not convened: nine one-line type edits and one spec, with both halves already proven by mutation.

Provenance

isAction is ours, not inherited: nuxt/ui@v4 has no such prop anywhere.

Gate

lint, typecheck, 322 test files / 7498 tests, test:module, and coverage 71.89 / 69.93 / 71.19 / 71.44 against thresholds of 70 / 68 / 70 / 70.

Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

claude added 2 commits August 30, 2026 06:27
…our it

`isAction` restyles a link as Bitrix24's dashed "action" text. `src/theme/link.ts`
is the only theme that implements it, and it is deliberately absent from
`linkKeys`, so `pickLinkProps` never forwards it downstream.

Everything spreading `LinkProps` inherited it anyway. A consumer could write
`isAction` on a `B24Button` or a breadcrumb item, TypeScript accepted it, Vue
registered it — `Button` declared 45 runtime props, one of them dead — and
nothing changed on screen. Eight types omit it now: `ButtonProps`,
`BreadcrumbItem`, `CommandPaletteItem`, `ContextMenuItem`, `DropdownMenuItem`,
`FooterColumnLink`, `NavigationMenuItem` and `PageLink`. Anything extending
`ButtonProps` follows.

Nothing in the repository passed it anywhere but `B24Link`, so no example,
playground or doc changes.

Not uniform, and the test says so rather than pretending otherwise:
`ButtonProps`, `PageLink` and `FooterColumnLink` have no index signature, so the
omission is enforceable. The five item types each declare `[key: string]: any`
so an item can carry arbitrary attributes — `keyof` therefore includes `string`,
and no `Omit` can make `isAction` unassignable on them. Their omission states
intent the compiler cannot check, and the spec asserts only the three it can.

The guard is in two halves because they fail under different commands: the
runtime assertions on `Button.props` and `linkKeys` go red under `vitest run`,
the type assertions only under `vue-tsc`. Both mutation-checked — reverting
`Button`'s omit reddens two tests, reverting `PageLink`'s reddens typecheck.

`isAction` is ours, not upstream's: `nuxt/ui@v4` has no such prop.
Review caught the sweep stopping short: it read `src/runtime/components/*.vue`
and never descended into `content/`, so `ContentSearchLink` and
`ContentSearchItem` kept advertising the prop. `ContentSearchLink` has no index
signature, which makes it one of the enforceable cases — it is asserted with
the other three, and reverting its omit reddens typecheck.

The sweep is now a search across all of `src/`, and it finds ten types in nine
files. That is the whole set.
@IgorShevchik
IgorShevchik merged commit 5c7ac86 into main Aug 30, 2026
3 checks passed
@IgorShevchik
IgorShevchik deleted the fix/is-action-not-inherited branch August 30, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants