ReviewOS

stacks/stx

Exported functions without an explicit return type ship as `: void` in the .d.ts (19 functions, incl. useLocalStorage)

#1796
Closed glennmichael123 opened this 10 days ago ·
10 days ago

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;                        // wrong

Cause

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:

SourceFunctions
composables/use-storage.tsuseLocalStorage, useSessionStorage
composables/use-idle.tsuseIdleState, useLastActive, useAutoLogout
composables/use-text-selection.tsuseElementTextSelection, useSelectionPopup, useCopySelection
composables/use-wake-lock.tsuseAutoWakeLock, useConditionalWakeLock
composables/use-device-orientation.tsuseParallax
composables/use-event-source.tsuseSSE
composables/use-eye-dropper.tsuseColorHistory
browser-composables.tsuseBreakpoints
ssr.tscreateApp
edge-runtime.tscreateEdgeHandler
visual-testing.tscreateSnapshotTester, 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.

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.

Sign in to comment on this issue.