Skip to content

fix(v4): export SmoothToRecord, the type the record overload returns - #873

Merged
titouanmathis merged 2 commits into
mainfrom
fix/v4-smoothto-record-type-export
Aug 26, 2026
Merged

fix(v4): export SmoothToRecord, the type the record overload returns#873
titouanmathis merged 2 commits into
mainfrom
fix/v4-smoothto-record-type-export

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

The defect

src/utils/smoothTo.ts declares three interfaces. The utils barrel named two of them:

export { smoothTo, type SmoothTo, type SmoothToOptions } from './smoothTo.js';

SmoothToRecord<K> is the return type of the record overload of smoothTo():

export function smoothTo(start?: number, options?: SmoothToOptions): SmoothTo;
export function smoothTo<K extends string>(
  start: Record<K, number>,
  options?: SmoothToOptions,
): SmoothToRecord<K>;

So it is part of the public signature, but it never reached dist. A consumer could call the overload and not write down what it got back:

import { smoothTo, type SmoothToRecord } from '@studiometa/js-toolkit/utils';
//                      ^ has no exported member 'SmoothToRecord'

Why it matters to a consumer

The return value of a public overload has to be nameable. Anything holding the record in a class field, a function parameter or a return position needs the type; inference only carries you as far as the first const. The @studiometa/ui v2 port hit exactly this and copied the interface into its own source — a duplicate that drifts from ours the first time either moves. That port is where the omission was found.

Scope

Every module under src/utils/ was audited against the barrel. SmoothToRecord is the only symbol a module exports and the barrel does not. This is a one-line fix, not a pattern.

What the public-surface checks obliged beyond the one line

Nothing in the generated surface, and two spec assertions.

  • npm run subpaths:check — unchanged, and deliberately so. enumerate() in scripts/lib/subpath-exports.js filters isType symbols out: a subpath exists to keep one runtime import from dragging in a whole barrel's graph, and a type import is erased before anything runs. So no stub, no exports entry. Types stay reachable through . and ./utils, which is exactly the entry point this PR fixes. Verified by running the check, not assumed.
  • check-doc-links.js — unchanged for the same reason: it walks src/subpaths/**/*.ts, and a type-only symbol has no stub to walk. It still reports its 193 exports. docs/utils/motion.md already documents the overload and already names SmoothToRecord<K> in its signature block, so there was no docs gap to close either.
  • The specs — both needed an assertion, because neither would have caught this:
    • src/utils/index.spec.ts compares Object.keys(barrel) against the runtime keys of every module it fronts. It is blind to types by construction. Its companion test, "forwards the types too, which the runtime check cannot see", is where the type belongs, and now names it.
    • src/exports.spec.ts reads the type through the published ./utils condition rather than a relative path, which is how the consumer sees it: expectTypeOf(smoothTo({ x: 0, y: 0 })).toEqualTypeOf<SmoothToRecord<'x' | 'y'>>().

Release

4.0.0-alpha.1 across the root manifest, @studiometa/js-toolkit and @studiometa/eslint-plugin-js-toolkit, with package-lock.json refreshed and a ### Fixed section in CHANGELOG.md.

Verification

Run from the repository root, on this branch.

$ npm run build
Building 263 modules...
Done building!
Building @studiometa/eslint-plugin-js-toolkit...
Done!
$ npm run lint
packages/js-toolkit: subpath stubs and exports map are up to date.
packages/js-toolkit/src/context.ts:53:35: warning unicorn(no-useless-spread): Using a spread
  operator here creates a new array unnecessarily.
Checking formatting...
All matched files use the correct format.
Finished in 2514ms on 567 files using 8 threads.

The single no-useless-spread warning in src/context.ts predates this branch and is untouched. lint:types passes for both workspaces.

$ npm test
Diagnostics: sinks are centralized and internal code references are tree-shakeable.
Doc links: 193 public exports each point at their documentation page.

 Test Files  113 passed (113)
      Tests  1570 passed (1570)
   Duration  38.58s
$ npm run check:package -w @studiometa/js-toolkit
Packed content: 596 files, 306.7 kB packed, 1051.4 kB unpacked.
Node packed consumer: root and public subpaths passed.
TypeScript packed consumer: diagnostics and watchAttributes types passed.
Browser packed consumer: Base lifecycle, events, helper subpaths, attribute watching, context
  subscription and service subpath passed.

And the emitted declaration now carries the type — dist/utils/index.d.ts:

export { …, type SmoothTo, type SmoothToOptions, type SmoothToRecord, … };

🤖 Generated with Claude Code

https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R

titouanmathis and others added 2 commits August 27, 2026 00:29
`smoothTo()` has two overloads. The scalar one returns `SmoothTo`, the
record one returns `SmoothToRecord<K>`. The utils barrel named the first
two interfaces of `smoothTo.ts` and stopped there, so the third never
reached `dist` and a consumer could call the record overload but not
write down what it got back:

  // before
  import { smoothTo, type SmoothToRecord } from '@studiometa/js-toolkit/utils';
  //                      ^ has no exported member 'SmoothToRecord'

That is not a cosmetic gap. The return value of a public overload is part
of the public signature: anything holding it in a class field, a function
parameter or a return position has to name it. The `@studiometa/ui` v2
port hit exactly this and copied the interface into its own source, which
is a duplicate that will drift from ours the first time either moves.

Nothing else in the package surface reacts to the line. A type-only
symbol gets no subpath — `enumerate()` filters `isType` out, because a
subpath exists to keep a runtime import from dragging in a barrel's graph
and a type import is erased before anything runs — so the stubs, the
`exports` map and `check-doc-links.js`, which walks those stubs, are all
unchanged. Types stay reachable through `.` and `./utils`, which is the
entry point this fixes.

The two specs that assert the surface now say so, since nothing else
would have caught it: `utils/index.spec.ts` compares runtime keys and is
blind to types by construction, and `exports.spec.ts` reads the type
through the published `./utils` condition rather than through a relative
path, which is the way the consumer sees it.

Every module under `src/utils/` was checked against the barrel; this was
the only symbol missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
A patch release carrying one fix: `SmoothToRecord` is exported from
`@studiometa/js-toolkit/utils`, so the return type of the record overload
of `smoothTo()` is nameable from the published package.

`@studiometa/eslint-plugin-js-toolkit` moves with the framework, as it did
at alpha.0. Nothing in it changed; the two packages publish from this
branch to the same `next` dist-tag and are easier to reason about when
their versions match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
@github-actions

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

✅ No export size changes.

Unchanged (196)

@studiometa/js-toolkit

Export Size (gzip) Diff
(barrel) 22.72 kB
BREAKPOINTS 774 B
Base 8.59 kB
DIAGNOSTICS 714 B
DRAG_MODES 162 B
EVENTS 153 B
MOUNT_ATTRIBUTE 69 B
SWAP_MODES 129 B
children 244 B
component 11.65 kB
createContext 468 B
createFallbackProvider 1.32 kB
createGroup 1.07 kB
createLocalStorage 2.32 kB
createMemoryStorageProvider 1.21 kB
createService 640 B
createServiceMixin 1015 B
createSessionStorage 2.32 kB
createStorage 2.3 kB
createUrlSearchParamsInHashProvider 1.21 kB
createUrlSearchParamsInHashStorage 2.34 kB
createUrlSearchParamsProvider 1.21 kB
createUrlSearchParamsStorage 2.34 kB
defaultScheduler 1.49 kB
defineManifest 979 B
domUpdate 1.23 kB
emitExtendable 1.08 kB
fromMetaGlob 203 B
fromWebpackContext 131 B
getBreakpoints 774 B
getInstance 128 B
getInstances 2.86 kB
getMountedInstances 2.87 kB
getUnmountedInstances 2.87 kB
inject 176 B
injectContext 672 B
injectContextSync 630 B
jsonSerializer 95 B
localStorageProvider 1.2 kB
memoryStorageProvider 1.21 kB
namespaceQualifier 120 B
nextFrame 115 B
on 8.93 kB
perTarget 322 B
provide 185 B
provideContext 700 B
provideRootContext 746 B
read 126 B
registerComponent 11.26 kB
registerComponents 11.27 kB
registerManifest 11.32 kB
reportDiagnostic 329 B
sessionStorageProvider 1.2 kB
setBreakpoints 803 B
signal 921 B
subscribeContext 1.43 kB
swap 2.89 kB
test 11.77 kB
toggle 177 B
until 172 B
urlSearchParamsInHashProvider 1.2 kB
urlSearchParamsProvider 1.2 kB
useBreakpoint 1.44 kB
useDrag 3.35 kB
useInView 1.42 kB
useKey 1.46 kB
useMediaQuery 1.05 kB
useMutation 1.4 kB
usePointer 1.85 kB
usePrefersReducedMotion 1.09 kB
useRaf 1.94 kB
useResize 1.42 kB
useScroll 2.67 kB
useScrollProgress 3.64 kB
useWindowScroll 2.65 kB
useWindowSize 1.43 kB
utils 9.25 kB
utils/DEFAULT_DAMP_FACTOR 109 B
utils/INERTIA_FRAME 97 B
utils/MAX_SPRING_RATIO 100 B
utils/SCROLL_ALIGNMENTS 117 B
utils/SCROLL_AXES 100 B
utils/TRANSFORM_PROPS 137 B
utils/TRANSITION_OPTIONS 132 B
utils/camelCase 449 B
utils/capitalize 119 B
utils/clamp 133 B
utils/clamp01 149 B
utils/clampDampFactor 157 B
utils/createEaseInOut 120 B
utils/createEaseOut 91 B
utils/createElement 638 B
utils/createRange 205 B
utils/damp 211 B
utils/debounce 121 B
utils/decayOver 162 B
utils/deepmerge 312 B
utils/easeInCirc 94 B
utils/easeInCubic 81 B
utils/easeInExpo 97 B
utils/easeInOutCirc 150 B
utils/easeInOutCubic 141 B
utils/easeInOutExpo 150 B
utils/easeInOutQuad 139 B
utils/easeInOutQuart 140 B
utils/easeInOutQuint 140 B
utils/easeInOutSine 156 B
utils/easeInQuad 80 B
utils/easeInQuart 81 B
utils/easeInQuint 81 B
utils/easeInSine 104 B
utils/easeLinear 77 B
utils/easeOutCirc 121 B
utils/easeOutCubic 111 B
utils/easeOutExpo 124 B
utils/easeOutQuad 110 B
utils/easeOutQuart 112 B
utils/easeOutQuint 111 B
utils/easeOutSine 132 B
utils/enterTransition 679 B
utils/fold 200 B
utils/getOffsetSizes 268 B
utils/historyPush 391 B
utils/historyReplace 392 B
utils/inertiaDecay 199 B
utils/inertiaFinalValue 187 B
utils/inertiaStep 232 B
utils/inertiaTimeConstant 178 B
utils/isBoolean 90 B
utils/isDefined 87 B
utils/isFunction 86 B
utils/isNull 78 B
utils/isNumber 103 B
utils/isObject 115 B
utils/isString 89 B
utils/kebabCase 421 B
utils/leaveTransition 679 B
utils/lerp 120 B
utils/loadImage 245 B
utils/loadLink 772 B
utils/loadScript 692 B
utils/lockScroll 561 B
utils/lowerCase 84 B
utils/map 128 B
utils/matrix 150 B
utils/mean 147 B
utils/memo 217 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 266 B
utils/pascalCase 434 B
utils/random 93 B
utils/randomInt 132 B
utils/randomItem 163 B
utils/round 130 B
utils/saveActiveElement 568 B
utils/scrollPosition 857 B
utils/scrollTo 1.86 kB
utils/selectorFor 2.77 kB
utils/setClassesOrStyles 218 B
utils/smoothTo 2.82 kB
utils/snakeCase 421 B
utils/spring 343 B
utils/throttle 151 B
utils/transform 286 B
utils/transition 577 B
utils/trapFocus 711 B
utils/untrapFocus 583 B
utils/upperCase 84 B
utils/wait 103 B
utils/withLeadingCharacters 142 B
utils/withLeadingSlash 152 B
utils/withTrailingCharacters 143 B
utils/withTrailingSlash 153 B
utils/withoutLeadingCharacters 127 B
utils/withoutLeadingCharactersRecursive 144 B
utils/withoutLeadingSlash 138 B
utils/withoutTrailingCharacters 129 B
utils/withoutTrailingCharactersRecursive 147 B
utils/withoutTrailingSlash 140 B
utils/wrap 154 B
viewTransition 1.66 kB
warn 321 B
watchAttributeNamespace 2.54 kB
watchAttributes 2.02 kB
whenDOMSettled 2.34 kB
withDrag 4.02 kB
withInView 2.11 kB
withKey 2.14 kB
withMutation 2.09 kB
withPointer 2.52 kB
withRaf 2.62 kB
withResize 2.1 kB
withScroll 3.33 kB
withScrollProgress 4.33 kB
write 124 B

@github-actions

Copy link
Copy Markdown

Code Review

Risk: Low — The change only exposes the missing SmoothToRecord type and adds coverage for the published utils entry point; it is safe to merge.

The utils barrel now re-exports SmoothToRecord, matching the return type of smoothTo's record overload. The added type assertions verify both the barrel forwarding and the public /utils package entry point.


Review usage: 8,758 in / 209 out tokens — $0.0061 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 90ec4b1.

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.55%. Comparing base (a0bd0d8) to head (90ec4b1).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #873   +/-   ##
=======================================
  Coverage   94.55%   94.55%           
=======================================
  Files          38       38           
  Lines        1176     1176           
  Branches      498      498           
=======================================
  Hits         1112     1112           
  Misses         57       57           
  Partials        7        7           
Flag Coverage Δ
eslint-plugin-js-toolkit 94.55% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

mount benchmarks

Base and head measured on this runner, alternating over 3 rounds each; every value is the median of the round medians. Running both sides on one machine is what removes cross-machine noise — a cached baseline from another runner would put it back.

A move under 25%, or on a benchmark under 5 ms, is not reported as a change: it is inside the measured noise of a shared runner.

No benchmark moved beyond the noise floor.

Within noise (18)
Group Benchmark Base Head us / component Change
mount 1000 components, one insertion control — declared but unregistered 1.90 ms 2.00 ms 2.00 +5.3%
mount 1000 components, one insertion flat 17.0 ms 17.3 ms 17.30 +1.8%
mount 1000 components, one insertion in-view — one controller per element 32.9 ms 31.0 ms 31.00 -5.8%
mount 1000 components, one insertion nested 4 deep 13.9 ms 13.8 ms 13.80 -0.7%
mount 1000 components, one insertion realistic — 5 refs, 3 options, 4 handlers 71.9 ms 74.7 ms 74.70 +3.9%
mount 1000 components, one insertion responsive option — breakpoint cascade per mount 16.6 ms 17.1 ms 17.10 +3.0%
mount 1000 flat components, 1 vs 10 insertions 1 insertion 11.2 ms 12.3 ms 12.30 +9.8%
mount 1000 flat components, 1 vs 10 insertions 10 insertions 12.1 ms 12.4 ms 12.40 +2.5%
mount 5000 components, one insertion control — declared but unregistered 17.8 ms 18.1 ms 3.62 +1.7%
mount 5000 components, one insertion flat 66.9 ms 68.7 ms 13.74 +2.7%
mount 5000 components, one insertion in-view — one controller per element 152.2 ms 173.5 ms 34.70 +14.0%
mount 5000 components, one insertion nested 4 deep 65.4 ms 68.0 ms 13.60 +4.0%
mount 5000 components, one insertion realistic — 5 refs, 3 options, 4 handlers 325.4 ms 334.6 ms 66.92 +2.8%
mount 5000 components, one insertion responsive option — breakpoint cascade per mount 84.6 ms 86.2 ms 17.24 +1.9%
mount 5000 flat components, 1 vs 10 insertions 1 insertion 66.7 ms 65.9 ms 13.18 -1.2%
mount 5000 flat components, 1 vs 10 insertions 10 insertions 66.0 ms 68.8 ms 13.76 +4.2%
unmount 1000 flat components, one removal flat 3.00 ms 3.20 ms 3.20 +6.7%
unmount 5000 flat components, one removal flat 15.5 ms 16.2 ms 3.24 +4.5%

@titouanmathis
titouanmathis merged commit 807c5f9 into main Aug 26, 2026
10 checks passed
@titouanmathis
titouanmathis deleted the fix/v4-smoothto-record-type-export branch August 26, 2026 22:40
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.

1 participant