diff --git a/.changeset/add-tkey-generic-parameter.md b/.changeset/add-tkey-generic-parameter.md new file mode 100644 index 000000000..40b1dba2f --- /dev/null +++ b/.changeset/add-tkey-generic-parameter.md @@ -0,0 +1,11 @@ +--- +'@tanstack/virtual-core': minor +'@tanstack/react-virtual': minor +'@tanstack/vue-virtual': minor +'@tanstack/solid-virtual': minor +'@tanstack/svelte-virtual': minor +'@tanstack/angular-virtual': minor +'@tanstack/lit-virtual': minor +--- + +Add `TKey` generic parameter to `VirtualItem`, `VirtualizerOptions`, and `Virtualizer` to allow narrowing the key type. This fixes a TypeScript error when using `virtualItem.key` as a React component key, since `bigint` in the default `Key` type is not assignable to React's `Key` type. diff --git a/packages/angular-virtual/src/index.ts b/packages/angular-virtual/src/index.ts index 55532654a..f0966d78a 100644 --- a/packages/angular-virtual/src/index.ts +++ b/packages/angular-virtual/src/index.ts @@ -18,7 +18,11 @@ import { } from '@tanstack/virtual-core' import { proxyVirtualizer } from './proxy' import type { ElementRef, Signal } from '@angular/core' -import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' +import type { + Key, + PartialKeys, + VirtualizerOptions, +} from '@tanstack/virtual-core' import type { AngularVirtualizer } from './types' export * from '@tanstack/virtual-core' @@ -27,10 +31,11 @@ export * from './types' function createVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, >( - options: Signal>, -): AngularVirtualizer { - let virtualizer: Virtualizer + options: Signal>, +): AngularVirtualizer { + let virtualizer: Virtualizer function lazyInit() { virtualizer ??= new Virtualizer(options()) return virtualizer @@ -81,14 +86,15 @@ function createVirtualizerBase< export function injectVirtualizer< TScrollElement extends Element, TItemElement extends Element, + TKey extends Key = Key, >( options: () => PartialKeys< - Omit, 'getScrollElement'>, + Omit, 'getScrollElement'>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' > & { scrollElement: ElementRef | TScrollElement | undefined }, -): AngularVirtualizer { +): AngularVirtualizer { const resolvedOptions = computed(() => { return { observeElementRect: observeElementRect, @@ -105,7 +111,7 @@ export function injectVirtualizer< ...options(), } }) - return createVirtualizerBase(resolvedOptions) + return createVirtualizerBase(resolvedOptions) } function isElementRef( @@ -114,15 +120,18 @@ function isElementRef( return elementOrRef != null && 'nativeElement' in elementOrRef } -export function injectWindowVirtualizer( +export function injectWindowVirtualizer< + TItemElement extends Element, + TKey extends Key = Key, +>( options: () => PartialKeys< - VirtualizerOptions, + VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): AngularVirtualizer { +): AngularVirtualizer { const resolvedOptions = computed(() => { return { getScrollElement: () => (typeof document !== 'undefined' ? window : null), @@ -134,5 +143,5 @@ export function injectWindowVirtualizer( ...options(), } }) - return createVirtualizerBase(resolvedOptions) + return createVirtualizerBase(resolvedOptions) } diff --git a/packages/angular-virtual/src/proxy.ts b/packages/angular-virtual/src/proxy.ts index 1664b58ea..0e6841676 100644 --- a/packages/angular-virtual/src/proxy.ts +++ b/packages/angular-virtual/src/proxy.ts @@ -1,16 +1,17 @@ import { computed, untracked } from '@angular/core' import type { Signal, WritableSignal } from '@angular/core' -import type { Virtualizer } from '@tanstack/virtual-core' +import type { Key, Virtualizer } from '@tanstack/virtual-core' import type { AngularVirtualizer } from './types' export function proxyVirtualizer< - V extends Virtualizer, - S extends Element | Window = V extends Virtualizer ? U : never, - I extends Element = V extends Virtualizer ? U : never, + V extends Virtualizer, + S extends Element | Window = V extends Virtualizer ? U : never, + I extends Element = V extends Virtualizer ? U : never, + K extends Key = V extends Virtualizer ? U : Key, >( virtualizerSignal: WritableSignal, lazyInit: () => V, -): AngularVirtualizer { +): AngularVirtualizer { return new Proxy(virtualizerSignal, { apply() { return virtualizerSignal() @@ -86,10 +87,10 @@ export function proxyVirtualizer< configurable: true, } }, - }) as unknown as AngularVirtualizer + }) as unknown as AngularVirtualizer } -function toComputed>( +function toComputed>( signal: Signal, fn: Function, ) { diff --git a/packages/angular-virtual/src/types.ts b/packages/angular-virtual/src/types.ts index 08c536cf7..a1903ed75 100644 --- a/packages/angular-virtual/src/types.ts +++ b/packages/angular-virtual/src/types.ts @@ -1,11 +1,12 @@ import type { Signal } from '@angular/core' -import type { Virtualizer } from '@tanstack/virtual-core' +import type { Key, Virtualizer } from '@tanstack/virtual-core' export type AngularVirtualizer< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, > = Omit< - Virtualizer, + Virtualizer, | 'getTotalSize' | 'getVirtualItems' | 'isScrolling' @@ -17,22 +18,22 @@ export type AngularVirtualizer< | 'scrollRect' > & { getTotalSize: Signal< - ReturnType['getTotalSize']> + ReturnType['getTotalSize']> > getVirtualItems: Signal< - ReturnType['getVirtualItems']> + ReturnType['getVirtualItems']> > - isScrolling: Signal['isScrolling']> - options: Signal['options']> - range: Signal['range']> + isScrolling: Signal['isScrolling']> + options: Signal['options']> + range: Signal['range']> scrollDirection: Signal< - Virtualizer['scrollDirection'] + Virtualizer['scrollDirection'] > scrollElement: Signal< - Virtualizer['scrollElement'] + Virtualizer['scrollElement'] > scrollOffset: Signal< - Virtualizer['scrollOffset'] + Virtualizer['scrollOffset'] > - scrollRect: Signal['scrollRect']> + scrollRect: Signal['scrollRect']> } diff --git a/packages/lit-virtual/src/index.ts b/packages/lit-virtual/src/index.ts index 963454fdb..699d056c8 100644 --- a/packages/lit-virtual/src/index.ts +++ b/packages/lit-virtual/src/index.ts @@ -8,23 +8,28 @@ import { windowScroll, } from '@tanstack/virtual-core' import type { ReactiveController, ReactiveControllerHost } from 'lit' -import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' +import type { + Key, + PartialKeys, + VirtualizerOptions, +} from '@tanstack/virtual-core' class VirtualizerControllerBase< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, > implements ReactiveController { host: ReactiveControllerHost - private readonly virtualizer: Virtualizer + private readonly virtualizer: Virtualizer private cleanup: () => void = () => {} constructor( host: ReactiveControllerHost, - options: VirtualizerOptions, + options: VirtualizerOptions, ) { - const resolvedOptions: VirtualizerOptions = { + const resolvedOptions: VirtualizerOptions = { ...options, onChange: (instance, sync) => { this.host.updateComplete.then(() => this.host.requestUpdate()) @@ -55,11 +60,12 @@ class VirtualizerControllerBase< export class VirtualizerController< TScrollElement extends Element, TItemElement extends Element, -> extends VirtualizerControllerBase { + TKey extends Key = Key, +> extends VirtualizerControllerBase { constructor( host: ReactiveControllerHost, options: PartialKeys< - VirtualizerOptions, + VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ) { @@ -74,11 +80,12 @@ export class VirtualizerController< export class WindowVirtualizerController< TItemElement extends Element, -> extends VirtualizerControllerBase { + TKey extends Key = Key, +> extends VirtualizerControllerBase { constructor( host: ReactiveControllerHost, options: PartialKeys< - VirtualizerOptions, + VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' diff --git a/packages/react-virtual/src/index.tsx b/packages/react-virtual/src/index.tsx index 313c3d4f9..fc60fcd08 100644 --- a/packages/react-virtual/src/index.tsx +++ b/packages/react-virtual/src/index.tsx @@ -9,7 +9,11 @@ import { observeWindowRect, windowScroll, } from '@tanstack/virtual-core' -import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' +import type { + Key, + PartialKeys, + VirtualizerOptions, +} from '@tanstack/virtual-core' export * from '@tanstack/virtual-core' @@ -19,23 +23,26 @@ const useIsomorphicLayoutEffect = export type ReactVirtualizerOptions< TScrollElement extends Element | Window, TItemElement extends Element, -> = VirtualizerOptions & { + TKey extends Key = Key, +> = VirtualizerOptions & { useFlushSync?: boolean } function useVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, >({ useFlushSync = true, ...options -}: ReactVirtualizerOptions): Virtualizer< +}: ReactVirtualizerOptions): Virtualizer< TScrollElement, - TItemElement + TItemElement, + TKey > { const rerender = React.useReducer(() => ({}), {})[1] - const resolvedOptions: VirtualizerOptions = { + const resolvedOptions: VirtualizerOptions = { ...options, onChange: (instance, sync) => { if (useFlushSync && sync) { @@ -48,7 +55,8 @@ function useVirtualizerBase< } const [instance] = React.useState( - () => new Virtualizer(resolvedOptions), + () => + new Virtualizer(resolvedOptions), ) instance.setOptions(resolvedOptions) @@ -67,13 +75,14 @@ function useVirtualizerBase< export function useVirtualizer< TScrollElement extends Element, TItemElement extends Element, + TKey extends Key = Key, >( options: PartialKeys< - ReactVirtualizerOptions, + ReactVirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): Virtualizer { - return useVirtualizerBase({ +): Virtualizer { + return useVirtualizerBase({ observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, @@ -81,16 +90,19 @@ export function useVirtualizer< }) } -export function useWindowVirtualizer( +export function useWindowVirtualizer< + TItemElement extends Element, + TKey extends Key = Key, +>( options: PartialKeys< - ReactVirtualizerOptions, + ReactVirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): Virtualizer { - return useVirtualizerBase({ +): Virtualizer { + return useVirtualizerBase({ getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, diff --git a/packages/solid-virtual/src/index.tsx b/packages/solid-virtual/src/index.tsx index 69ac34fd7..6f0d97aec 100644 --- a/packages/solid-virtual/src/index.tsx +++ b/packages/solid-virtual/src/index.tsx @@ -16,20 +16,25 @@ import { onMount, } from 'solid-js' import { createStore, reconcile } from 'solid-js/store' -import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' +import type { + Key, + PartialKeys, + VirtualizerOptions, +} from '@tanstack/virtual-core' export * from '@tanstack/virtual-core' function createVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, >( - options: VirtualizerOptions, -): Virtualizer { - const resolvedOptions: VirtualizerOptions = + options: VirtualizerOptions, +): Virtualizer { + const resolvedOptions: VirtualizerOptions = mergeProps(options) - const instance = new Virtualizer( + const instance = new Virtualizer( resolvedOptions, ) @@ -40,8 +45,8 @@ function createVirtualizerBase< const handler = { get( - target: Virtualizer, - prop: keyof Virtualizer, + target: Virtualizer, + prop: keyof Virtualizer, ) { switch (prop) { case 'getVirtualItems': @@ -67,7 +72,7 @@ function createVirtualizerBase< virtualizer.setOptions( mergeProps(resolvedOptions, options, { onChange: ( - instance: Virtualizer, + instance: Virtualizer, sync: boolean, ) => { instance._willUpdate() @@ -90,13 +95,14 @@ function createVirtualizerBase< export function createVirtualizer< TScrollElement extends Element, TItemElement extends Element, + TKey extends Key = Key, >( options: PartialKeys< - VirtualizerOptions, + VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): Virtualizer { - return createVirtualizerBase( +): Virtualizer { + return createVirtualizerBase( mergeProps( { observeElementRect: observeElementRect, @@ -108,16 +114,19 @@ export function createVirtualizer< ) } -export function createWindowVirtualizer( +export function createWindowVirtualizer< + TItemElement extends Element, + TKey extends Key = Key, +>( options: PartialKeys< - VirtualizerOptions, + VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): Virtualizer { - return createVirtualizerBase( +): Virtualizer { + return createVirtualizerBase( mergeProps( { getScrollElement: () => diff --git a/packages/svelte-virtual/src/index.ts b/packages/svelte-virtual/src/index.ts index f16aa03ce..2aa8bb655 100644 --- a/packages/svelte-virtual/src/index.ts +++ b/packages/svelte-virtual/src/index.ts @@ -8,7 +8,11 @@ import { windowScroll, } from '@tanstack/virtual-core' import { derived, writable } from 'svelte/store' -import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' +import type { + Key, + PartialKeys, + VirtualizerOptions, +} from '@tanstack/virtual-core' import type { Readable, Writable } from 'svelte/store' export * from '@tanstack/virtual-core' @@ -16,26 +20,28 @@ export * from '@tanstack/virtual-core' export type SvelteVirtualizer< TScrollElement extends Element | Window, TItemElement extends Element, -> = Omit, 'setOptions'> & { + TKey extends Key = Key, +> = Omit, 'setOptions'> & { setOptions: ( - options: Partial>, + options: Partial>, ) => void } function createVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, >( - initialOptions: VirtualizerOptions, -): Readable> { + initialOptions: VirtualizerOptions, +): Readable> { const virtualizer = new Virtualizer(initialOptions) const originalSetOptions = virtualizer.setOptions // eslint-disable-next-line prefer-const - let virtualizerWritable: Writable> + let virtualizerWritable: Writable> const setOptions = ( - options: Partial>, + options: Partial>, ) => { const resolvedOptions = { ...virtualizer.options, @@ -45,7 +51,7 @@ function createVirtualizerBase< originalSetOptions({ ...resolvedOptions, onChange: ( - instance: Virtualizer, + instance: Virtualizer, sync: boolean, ) => { virtualizerWritable.set(instance) @@ -68,13 +74,14 @@ function createVirtualizerBase< export function createVirtualizer< TScrollElement extends Element, TItemElement extends Element, + TKey extends Key = Key, >( options: PartialKeys< - VirtualizerOptions, + VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): Readable> { - return createVirtualizerBase({ +): Readable> { + return createVirtualizerBase({ observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, @@ -82,16 +89,19 @@ export function createVirtualizer< }) } -export function createWindowVirtualizer( +export function createWindowVirtualizer< + TItemElement extends Element, + TKey extends Key = Key, +>( options: PartialKeys< - VirtualizerOptions, + VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, -): Readable> { - return createVirtualizerBase({ +): Readable> { + return createVirtualizerBase({ getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index bc966a6ce..7b47e23c8 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -26,10 +26,10 @@ export interface Range { count: number } -type Key = number | string | bigint +export type Key = number | string | bigint -export interface VirtualItem { - key: Key +export interface VirtualItem { + key: TKey index: number start: number end: number @@ -243,7 +243,7 @@ export const observeWindowOffset = ( export const measureElement = ( element: TItemElement, entry: ResizeObserverEntry | undefined, - instance: Virtualizer, + instance: Virtualizer, ) => { if (entry?.borderBoxSize) { const box = entry.borderBoxSize[0] @@ -295,6 +295,7 @@ export const elementScroll = ( export interface VirtualizerOptions< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, > { // Required from the user count: number @@ -305,27 +306,27 @@ export interface VirtualizerOptions< scrollToFn: ( offset: number, options: { adjustments?: number; behavior?: ScrollBehavior }, - instance: Virtualizer, + instance: Virtualizer, ) => void observeElementRect: ( - instance: Virtualizer, + instance: Virtualizer, cb: (rect: Rect) => void, ) => void | (() => void) observeElementOffset: ( - instance: Virtualizer, + instance: Virtualizer, cb: ObserveOffsetCallBack, ) => void | (() => void) // Optional debug?: boolean initialRect?: Rect onChange?: ( - instance: Virtualizer, + instance: Virtualizer, sync: boolean, ) => void measureElement?: ( element: TItemElement, entry: ResizeObserverEntry | undefined, - instance: Virtualizer, + instance: Virtualizer, ) => number overscan?: number horizontal?: boolean @@ -334,12 +335,12 @@ export interface VirtualizerOptions< scrollPaddingStart?: number scrollPaddingEnd?: number initialOffset?: number | (() => number) - getItemKey?: (index: number) => Key + getItemKey?: (index: number) => TKey rangeExtractor?: (range: Range) => Array scrollMargin?: number gap?: number indexAttribute?: string - initialMeasurementsCache?: Array + initialMeasurementsCache?: Array> lanes?: number isScrollingResetDelay?: number useScrollendEvent?: boolean @@ -367,15 +368,16 @@ type ScrollState = { export class Virtualizer< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, > { private unsubs: Array void)> = [] - options!: Required> + options!: Required> scrollElement: TScrollElement | null = null targetWindow: (Window & typeof globalThis) | null = null isScrolling = false private scrollState: ScrollState | null = null - measurementsCache: Array = [] - private itemSizeCache = new Map() + measurementsCache: Array> = [] + private itemSizeCache = new Map() private laneAssignments = new Map() // index → lane cache private pendingMeasuredCacheIndexes: Array = [] private prevLanes: number | undefined = undefined @@ -388,11 +390,11 @@ export class Virtualizer< shouldAdjustScrollPositionOnItemSizeChange: | undefined | (( - item: VirtualItem, + item: VirtualItem, delta: number, - instance: Virtualizer, + instance: Virtualizer, ) => boolean) - elementsCache = new Map() + elementsCache = new Map() private now = () => this.targetWindow?.performance?.now?.() ?? Date.now() private observer = (() => { let _ro: ResizeObserver | null = null @@ -430,11 +432,11 @@ export class Virtualizer< })() range: { startIndex: number; endIndex: number } | null = null - constructor(opts: VirtualizerOptions) { + constructor(opts: VirtualizerOptions) { this.setOptions(opts) } - setOptions = (opts: VirtualizerOptions) => { + setOptions = (opts: VirtualizerOptions) => { Object.entries(opts).forEach(([key, value]) => { if (typeof value === 'undefined') delete (opts as any)[key] }) @@ -448,7 +450,7 @@ export class Virtualizer< scrollPaddingStart: 0, scrollPaddingEnd: 0, horizontal: false, - getItemKey: defaultKeyExtractor, + getItemKey: defaultKeyExtractor as (index: number) => TKey, rangeExtractor: defaultRangeExtractor, onChange: () => {}, measureElement, @@ -666,11 +668,11 @@ export class Virtualizer< } private getFurthestMeasurement = ( - measurements: Array, + measurements: Array>, index: number, ) => { const furthestMeasurementsFound = new Map() - const furthestMeasurements = new Map() + const furthestMeasurements = new Map>() for (let m = index - 1; m >= 0; m--) { const measurement = measurements[m]! @@ -1062,7 +1064,7 @@ export class Virtualizer< getVirtualItems = memo( () => [this.getVirtualIndexes(), this.getMeasurements()], (indexes, measurements) => { - const virtualItems: Array = [] + const virtualItems: Array> = [] for (let k = 0, len = indexes.length; k < len; k++) { const i = indexes[k]! @@ -1332,7 +1334,7 @@ function calculateRange({ scrollOffset, lanes, }: { - measurements: Array + measurements: Array> outerSize: number scrollOffset: number lanes: number diff --git a/packages/vue-virtual/src/index.ts b/packages/vue-virtual/src/index.ts index 57f73311c..28c196de8 100644 --- a/packages/vue-virtual/src/index.ts +++ b/packages/vue-virtual/src/index.ts @@ -15,7 +15,11 @@ import { unref, watch, } from 'vue' -import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' +import type { + Key, + PartialKeys, + VirtualizerOptions, +} from '@tanstack/virtual-core' import type { Ref } from 'vue' export * from '@tanstack/virtual-core' @@ -25,9 +29,10 @@ type MaybeRef = T | Ref function useVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, + TKey extends Key = Key, >( - options: MaybeRef>, -): Ref> { + options: MaybeRef>, +): Ref> { const virtualizer = new Virtualizer(unref(options)) const state = shallowRef(virtualizer) @@ -72,15 +77,16 @@ function useVirtualizerBase< export function useVirtualizer< TScrollElement extends Element, TItemElement extends Element, + TKey extends Key = Key, >( options: MaybeRef< PartialKeys< - VirtualizerOptions, + VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' > >, -): Ref> { - return useVirtualizerBase( +): Ref> { + return useVirtualizerBase( computed(() => ({ observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, @@ -90,18 +96,21 @@ export function useVirtualizer< ) } -export function useWindowVirtualizer( +export function useWindowVirtualizer< + TItemElement extends Element, + TKey extends Key = Key, +>( options: MaybeRef< PartialKeys< - VirtualizerOptions, + VirtualizerOptions, | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' | 'getScrollElement' > >, -): Ref> { - return useVirtualizerBase( +): Ref> { + return useVirtualizerBase( computed(() => ({ getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect,