chore(superset-ui-core): forward-compat fixes for TypeScript 6.0

Scoped to packages/superset-ui-core. All changes compile cleanly on
TypeScript 5.4.5 (current CI) and eliminate TS 6.0 errors in this package.

Key fixes:

- forwardRef callbacks use ForwardedRef<T> instead of RefObject<T>
  (AsyncEsmComponent, DropdownContainer, ModalTrigger). ModalTrigger
  also narrows object-ref access with a typeof-function guard.
- StatefulChart: cast onRenderFailure to the shared HandlerFunction type
  so the stricter 6.0 inference matches the SuperChart prop.
- AntdEnhanced: drop the wider IconType.component from the spread so
  {...rest} can no longer override the explicit <BaseIconComponent
  component={...}> binding under 6.0's stricter JSX attribute checking.
- Select / AsyncSelect: widen antd sorter/filter/handler call-sites with
  targeted `as unknown as ...` casts against antd's BaseOptionType /
  DefaultOptionType / SelectHandler shapes. Behaviour unchanged; only
  the type boundary moves.
- VirtualTable: accept width?: number from react-resize-detector's 6.0
  callback signature, defaulting missing width to 0.
- TableCollection: cast columns / rowClassName / onChange to antd's
  generic ColumnsType<object> and TableProps<object> variants.
- TimezoneSelector: cast our comparator to antd's LabeledValue-based
  sorter signature; our comparator only reads fields that always exist
  on TimezoneOption, so the broader shape is safe at runtime.
- connection/types: widen FetchRetryOptions retryDelay/retryOn to
  accept `Error | null` / `Response | null`, matching fetch-retry.
- fetchTimeRange: rename catch variable and narrow it via the
  getClientErrorObject parameter type, handling TS 6.0's `unknown`
  default for caught values.
- lruCache: guard Map iterator .value before Map#delete (TS 6.0 types
  IteratorResult.value as `string | undefined`).
- InteractiveTableUtils: initialise columnRef to null for strict
  property initialization.
- types/assets.d.ts: add `declare module '*.css'` for CSS side-effect
  imports under 6.0's stricter module resolution.

Part of the TypeScript 5.4 -> 6.0 migration, split per-package to keep
reviews small. No runtime behaviour changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-04-21 18:37:04 -07:00
parent 05fc5bb424
commit 9b5d89bfaa
15 changed files with 190 additions and 45 deletions

View File

@@ -28,6 +28,7 @@ import {
RequestConfig,
getClientErrorObject,
} from '../..';
import type { HandlerFunction } from '../types/Base';
import { Loading } from '../../components/Loading';
import ChartClient from '../clients/ChartClient';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
@@ -482,7 +483,7 @@ export default function StatefulChart(props: StatefulChartProps) {
enableNoResults={enableNoResults}
noResults={NoDataComponent && <NoDataComponent />}
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure}
onRenderFailure={onRenderFailure as HandlerFunction | undefined}
hooks={hooks}
/>
);

View File

@@ -19,7 +19,7 @@
import {
useEffect,
useState,
RefObject,
ForwardedRef,
forwardRef,
ComponentType,
ForwardRefExoticComponent,
@@ -101,7 +101,7 @@ export function AsyncEsmComponent<
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps,
ref: RefObject<ComponentType<FullProps>>,
ref: ForwardedRef<ComponentType<FullProps>>,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {

View File

@@ -19,7 +19,7 @@
import {
cloneElement,
forwardRef,
RefObject,
ForwardedRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
@@ -54,7 +54,7 @@ export const DropdownContainer = forwardRef(
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
outerRef: ForwardedRef<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();

View File

@@ -326,11 +326,17 @@ export const antdEnhancedIcons: Record<
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
.reduce(
(acc, key) => {
acc[key as AntdIconNames] = (props: IconType) => (
acc[key as AntdIconNames] = ({
// Forward-compat: TS 6.0 treats IconComponentProps.component as a
// different shape than BaseIconProps.component; strip it from spread
// props so our own component binding is authoritative.
component: _ignoredComponent,
...rest
}: IconType) => (
<BaseIconComponent
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
{...rest}
/>
);
return acc;

View File

@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef, useState, ReactNode, MouseEvent } from 'react';
import {
forwardRef,
ForwardedRef,
useState,
ReactNode,
MouseEvent,
} from 'react';
import { Button } from '../Button';
import { Modal } from '../Modal';
@@ -51,7 +57,7 @@ export interface ModalTriggerRef {
}
export const ModalTrigger = forwardRef(
(props: ModalTriggerProps, ref: ModalTriggerRef | null) => {
(props: ModalTriggerProps, ref: ForwardedRef<ModalTriggerRef['current']>) => {
const [showModal, setShowModal] = useState(false);
const {
beforeOpen = () => {},
@@ -84,7 +90,7 @@ export const ModalTrigger = forwardRef(
setShowModal(true);
};
if (ref) {
if (ref && typeof ref !== 'function') {
ref.current = { close, open, showModal }; // eslint-disable-line
}

View File

@@ -18,6 +18,7 @@
*/
import {
forwardRef,
ForwardedRef,
FocusEvent,
ReactElement,
RefObject,
@@ -38,6 +39,8 @@ import {
getClientErrorObject,
} from '@superset-ui/core';
import {
BaseOptionType,
DefaultOptionType,
LabeledValue as AntdLabeledValue,
RefSelectProps,
} from 'antd/es/select';
@@ -146,7 +149,7 @@ const AsyncSelect = forwardRef(
maxTagCount: propsMaxTagCount,
...props
}: AsyncSelectProps,
ref: RefObject<AsyncSelectRef>,
ref: ForwardedRef<AsyncSelectRef>,
) => {
const isSingleMode = mode === 'single';
const [selectValue, setSelectValue] = useState(value);
@@ -307,7 +310,14 @@ const AsyncSelect = forwardRef(
mergedData = prevOptions
.filter(previousOption => !dataValues.has(previousOption.value))
.concat(data)
.sort(sortComparatorForNoSearch);
// Forward-compat: TS 6.0 infers stricter antd option types; widen
// the comparator to accept the broader DefaultOptionType shape.
.sort(
sortComparatorForNoSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
);
return mergedData;
});
}
@@ -435,7 +445,13 @@ const AsyncSelect = forwardRef(
if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
const sortedOptions = selectOptions
.slice()
.sort(sortComparatorForNoSearch);
// Forward-compat: see note in mergeData above.
.sort(
sortComparatorForNoSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
);
if (!isEqual(sortedOptions, selectOptions)) {
setSelectOptions(sortedOptions);
}
@@ -533,14 +549,16 @@ const AsyncSelect = forwardRef(
const clearCache = () => fetchedQueries.current.clear();
useImperativeHandle(
ref,
() => ({
...(ref.current as RefSelectProps),
useImperativeHandle(ref, () => {
const current =
ref && typeof ref !== 'function' && ref.current
? (ref.current as RefSelectProps)
: ({} as RefSelectProps);
return {
...current,
clearCache,
}),
[ref],
);
};
}, [ref]);
const getPastedTextValue = useCallback(
async (text: string) => {
@@ -606,8 +624,21 @@ const AsyncSelect = forwardRef(
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
popupRender={popupRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
// Forward-compat: TS 6.0 infers stricter antd option types; local
// helpers typed against AntdLabeledValue are behaviorally compatible
// with the broader BaseOptionType/DefaultOptionType antd expects.
filterOption={
handleFilterOption as unknown as (
search: string,
option?: BaseOptionType | DefaultOptionType,
) => boolean
}
filterSort={
sortComparatorWithSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number
}
getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode)
}
@@ -617,13 +648,26 @@ const AsyncSelect = forwardRef(
mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
// Forward-compat: TS 6.0 narrows the Select value type handed to
// SelectHandler; our local handlers already accept the broader union.
onDeselect={
handleOnDeselect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onOpenChange={handleOnDropdownVisibleChange}
// @ts-expect-error
// @ts-expect-error antd Select does not declare onPaste on its prop
// surface, but the underlying input accepts it and we rely on that.
onPaste={onPaste}
onPopupScroll={handlePagination}
onSearch={showSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect}
onSelect={
handleOnSelect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onClear={handleClear}
options={fullSelectOptions}
optionRender={option => <Space>{option.label || option.value}</Space>}

View File

@@ -34,6 +34,8 @@ import { t } from '@apache-superset/core/translation';
import { ensureIsArray, formatNumber, usePrevious } from '@superset-ui/core';
import { Constants } from '@superset-ui/core/components';
import {
BaseOptionType,
DefaultOptionType,
LabeledValue as AntdLabeledValue,
RefSelectProps,
} from 'antd/es/select';
@@ -211,7 +213,17 @@ const Select = forwardRef(
);
const initialOptionsSorted = useMemo(
() => initialOptions.slice().sort(sortSelectedFirst),
() =>
initialOptions
.slice()
// Forward-compat: TS 6.0 infers stricter antd option types; widen the
// comparator to accept the broader DefaultOptionType shape.
.sort(
sortSelectedFirst as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
),
[initialOptions, sortSelectedFirst],
);
@@ -239,7 +251,17 @@ const Select = forwardRef(
missingValues.length > 0
? missingValues.concat(selectOptions)
: selectOptions;
return result.slice().sort(sortSelectedFirst);
return (
result
.slice()
// Forward-compat: see note on initialOptionsSorted.
.sort(
sortSelectedFirst as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
)
);
}, [selectOptions, selectValue, sortSelectedFirst]);
const enabledOptions = useMemo(
@@ -751,8 +773,21 @@ const Select = forwardRef(
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
popupRender={popupRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
// Forward-compat: TS 6.0 infers stricter antd option types; local
// helpers typed against AntdLabeledValue are behaviorally compatible
// with the broader BaseOptionType/DefaultOptionType antd expects.
filterOption={
handleFilterOption as unknown as (
search: string,
option?: BaseOptionType | DefaultOptionType,
) => boolean
}
filterSort={
sortComparatorWithSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number
}
getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode)
}
@@ -763,13 +798,26 @@ const Select = forwardRef(
mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
// Forward-compat: TS 6.0 narrows the Select value type handed to
// SelectHandler; our local handlers already accept the broader union.
onDeselect={
handleOnDeselect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onOpenChange={handleOnDropdownVisibleChange}
// @ts-expect-error
// @ts-expect-error antd Select does not declare onPaste on its prop
// surface, but the underlying input accepts it and we rely on that.
onPaste={onPaste}
onPopupScroll={undefined}
onSearch={shouldShowSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect}
onSelect={
handleOnSelect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onClear={handleClear}
placeholder={placeholder}
tokenSeparators={tokenSeparators}

View File

@@ -84,8 +84,8 @@ const VirtualTable = <RecordType extends object>(
allowHTML = false,
} = props;
const [tableWidth, setTableWidth] = useState<number>(0);
const onResize = useCallback((width: number) => {
setTableWidth(width);
const onResize = useCallback((width?: number) => {
setTableWidth(width ?? 0);
}, []);
const { ref } = useResizeDetector({ onResize });
const theme = useTheme();

View File

@@ -29,7 +29,7 @@ interface IInteractiveColumn extends HTMLElement {
export default class InteractiveTableUtils {
tableRef: HTMLTableElement | null;
columnRef: IInteractiveColumn | null;
columnRef: IInteractiveColumn | null = null;
setDerivedColumns: Function;

View File

@@ -27,7 +27,12 @@ import {
} from 'react-table';
import { styled } from '@apache-superset/core/theme';
import { Table, TableSize } from '@superset-ui/core/components/Table';
import { TableRowSelection, SorterResult } from 'antd/es/table/interface';
import {
ColumnsType,
TableRowSelection,
SorterResult,
} from 'antd/es/table/interface';
import type { TableProps } from 'antd/es/table';
import { mapColumns, mapRows } from './utils';
interface TableCollectionProps<T extends object> {
@@ -290,7 +295,10 @@ function TableCollection<T extends object>({
<StyledTable
loading={loading}
sticky={sticky ?? false}
columns={mappedColumns}
// Forward-compat: TS 6.0 tightens antd Table's generic inference so our
// typed-against-react-table mapped columns must be widened to the antd
// ColumnsType<object> surface the Table expects here.
columns={mappedColumns as unknown as ColumnsType<object>}
data={mappedRows}
size={size}
data-test="listview-table"
@@ -303,7 +311,9 @@ function TableCollection<T extends object>({
sortDirections={['ascend', 'descend', 'ascend']}
isPaginationSticky={isPaginationSticky}
showRowCount={showRowCount}
rowClassName={getRowClassName}
rowClassName={
getRowClassName as unknown as TableProps<object>['rowClassName']
}
components={{
header: {
cell: (props: HTMLAttributes<HTMLTableCellElement>) => (
@@ -319,7 +329,7 @@ function TableCollection<T extends object>({
),
},
}}
onChange={handleTableChange}
onChange={handleTableChange as unknown as TableProps<object>['onChange']}
/>
);
}

View File

@@ -26,6 +26,7 @@ import {
getOffsetKey,
DEFAULT_TIMEZONE,
} from './TimezoneOptionsCache';
import type { LabeledValue } from 'antd/es/select';
import type { TimezoneOption } from './types';
// Import dayjs plugin types for TypeScript support
@@ -156,7 +157,16 @@ export default function TimezoneSelector({
onOpenChange={handleOpenChange}
value={selectValue}
options={timezoneOptions || []}
sortComparator={sortComparator}
// Forward-compat: TS 6.0 resolves sortComparator against antd's
// LabeledValue; our comparator only reads properties that always exist
// on TimezoneOption, so the broader shape is safe at runtime.
sortComparator={
sortComparator as unknown as (
a: LabeledValue,
b: LabeledValue,
search?: string,
) => number
}
loading={isLoadingOptions}
placeholder={isLoadingOptions ? t('Loading timezones...') : placeholder}
{...{ placement: 'topLeft', ...rest }}

View File

@@ -26,10 +26,18 @@ export type FetchRetryOptions = {
retries?: number;
retryDelay?:
| number
| ((attempt: number, error: Error, response: Response) => number);
| ((
attempt: number,
error: Error | null,
response: Response | null,
) => number);
retryOn?:
| number[]
| ((attempt: number, error: Error, response: Response) => boolean);
| ((
attempt: number,
error: Error | null,
response: Response | null,
) => boolean);
};
export type Headers = { [k: string]: string };
export type Host = string;

View File

@@ -103,10 +103,16 @@ export const fetchTimeRange = async (
),
),
};
} catch (response) {
} catch (caught) {
// Forward-compat: TS 6.0 types caught values as `unknown`; cast to the
// shape getClientErrorObject accepts and narrow for statusText access.
const response = caught as Parameters<typeof getClientErrorObject>[0];
const clientError = await getClientErrorObject(response);
return {
error: clientError.message || clientError.error || response.statusText,
error:
clientError.message ||
clientError.error ||
(response as { statusText?: string }).statusText,
};
}
};

View File

@@ -55,7 +55,12 @@ class LRUCache<T> {
throw new TypeError('The LRUCache key must be string.');
}
if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
// Forward-compat: TS 6.0 types IteratorResult.value as `string | undefined`
// when not explicitly checked; guard before passing to Map#delete.
const oldestKey = this.cache.keys().next().value;
if (oldestKey !== undefined) {
this.cache.delete(oldestKey);
}
}
this.cache.set(key, value);
}

View File

@@ -21,3 +21,4 @@ declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.css';