Verified against @stacksjs/stx@0.2.146, as resolved in a real app's node_modules.
Symptom
import { useLocalStorage } from '@stacksjs/stx/composables'
const s = useLocalStorage('k', 'v')
s.value // error TS2339: Property 'value' does not exist on type 'void'.useLocalStorage plainly returns a StorageRef<T>. The shipped declaration disagrees:
// dist/composables/use-storage.d.ts
export declare function useStorage<T>(key: string, defaultValue: T, options?: UseStorageOptions<T>): StorageRef<T>; // correct
export declare function useLocalStorage<T>(key: string, defaultValue: T, options?: …): void; // wrong
export declare function useSessionStorage<T>(key: string, defaultValue: T, options?: …): void; // wrongCause
The difference between the correct line and the broken ones is that useStorage carries an explicit return type annotation in the source and the two shorthands do not:
export function useStorage<T>(…): StorageRef<T> { … } // annotated
export function useLocalStorage<T>(…) { // inferred
return useStorage(key, defaultValue, { ...options, storage: 'local' })
}bun-plugin-dtsx is a syntactic emitter — it does not run a type checker, so it cannot resolve an inferred return type and falls back to void. Any exported function that returns a value without annotating it is silently mistyped for every consumer.
Scope
Cross-checking all 206 export declare function …: void entries in the shipped dist against the source, 19 functions return a value but ship as void:
| Source | Functions |
|---|---|
composables/use-storage.ts | useLocalStorage, useSessionStorage |
composables/use-idle.ts | useIdleState, useLastActive, useAutoLogout |
composables/use-text-selection.ts | useElementTextSelection, useSelectionPopup, useCopySelection |
composables/use-wake-lock.ts | useAutoWakeLock, useConditionalWakeLock |
composables/use-device-orientation.ts | useParallax |
composables/use-event-source.ts | useSSE |
composables/use-eye-dropper.ts | useColorHistory |
browser-composables.ts | useBreakpoints |
ssr.ts | createApp |
edge-runtime.ts | createEdgeHandler |
visual-testing.ts | createSnapshotTester, createStoryTester, createVisualRegressionTester |
The remaining ~187 are honest voids.
Fix
Annotate the return types explicitly. tsc --declaration gives the authoritative types to transplant. A guard test that fails on any exported function which returns a value without an annotation keeps it from recurring — that is the exact precondition dtsx needs.
Related
While confirming this, note useLocalStorage currently has five different return shapes across the monorepo and the Stacks ecosystem — StxSignal<T> (ambient stx.d.ts + the signals runtime), StorageRef<T> (composables/use-storage.ts), { value, remove } (browser-composables.ts), { value, setValue, removeValue } (packages/components), and Ref<T> (@stacksjs/composables). That is a separate issue from this one, but it is why the void went unnoticed: there was no single expected shape to check against.