admin管理员组

文章数量:1417107

The title will make more sense with an example:

I have the following component GlobalDialog:

export type GlobalDialogProps<T extends React.FunctionComponent<any>> = {
    Trigger: React.FunctionComponent<{ onToggle: () => void; }>;
    Panel: T;
    panelProps: ComponentProps<T>;
    zIndex: number;
};

export default function GlobalDialog<T extends React.FunctionComponent<any>>(props: GlobalDialogProps<T>) {
    // Implementation details
}

and component FilterProductResultsControllerV2

export default function FilterProductResultsControllerV2<T extends React.FC<any>>(props: { ResultElement: T; propsFactory: (product: IProduct) => ComponentProps<T>; }) {
    // Implementation details
}

I then try to pass FilterProductResultsControllerV2 as a Panel to my Global Dialog, and propsFactory ends up being inferred as (product: IProduct) => any instead of (product: IProduct) => CountTrackerProductProps

<GlobalDialog
    zIndex={10}
    Trigger={({ onToggle }) => (
        // Implementation detail
    )}
    Panel={FilterProductResultsControllerV2}
    panelProps={{
        renderAs: "panel",
        ResultElement: CountTrackerProduct,
        propsFactory: (product) => {
            const orderItemData = value[product.id];

            return {
                product: product,
                onAdd: () => addOrderItem(product),
                quantity: orderItemData?.quantity ?? null,
            };
        },
    }}
/>

where CountTrackerProduct is:

type CountTrackerProductProps = { product: IProduct; onAdd: () => void; quantity: number | null; };

export default function CountTrackerProduct(props: CountTrackerProductProps) { 
    // Implementation details
}

A working solution to this problem is using FilterProductResultsControllerV2<typeof CountTrackerProduct> as Panel, but it's not the most elegant one either.

So my question is, why exactly does this happen in Typescript? and are there any ways around it, if any?

Full Sample Code that features the issue

import { useState } from "react";
    
interface IProduct {
    id: string;
    name: string;
}

type GlobalDialogProps<T extends React.FunctionComponent<any>> = {
    Trigger: React.FunctionComponent<{ onToggle: () => void }>;
    Panel: T;
    panelProps: React.ComponentProps<T>;
    zIndex: number;
};

function GlobalDialog<T extends React.FunctionComponent<any>>(props: GlobalDialogProps<T>) {
    const [open, setOpen] = useState(false);
    const { Panel, panelProps, Trigger } = props;
    return (
        <>
            <Trigger onToggle={() => setOpen(!open)} />
            {/*<Panel {...panelProps} /> */}
        </>
    );
}

function FilterProductResultsControllerV2<T extends React.FC<any>>(props: { ResultElement: T; propsFactory: (product: IProduct) => React.ComponentProps<T> }) {
    return <div></div>;
}

type CountTrackerProductProps = {
    product: IProduct;
    onAdd: () => void;
    quantity: number | null;
};

function CountTrackerProduct(props: CountTrackerProductProps) {
    return <div></div>;
}

function OrderItemDataWidget() {
    const [value, setValue] = useState<Record<IProduct["id"], { quantity: number }>>({});
    const addOrderItem = (product: IProduct) => console.log(`added product ${product.id}`);

    return (
        <div>
            <div>close off</div>
            <GlobalDialog
                zIndex={10}
                Trigger={({ onToggle }) => <button onClick={onToggle} type="button">dummy</button>}
                Panel={FilterProductResultsControllerV2}
                panelProps={{
                    ResultElement: CountTrackerProduct,
                    propsFactory: (product) => {
                        const orderItemData = value[product.id];

                        return {
                            product: product,
                            onAdd: () => addOrderItem(product),
                            quantity: orderItemData?.quantity ?? null,
                        };
                    },
                }}
            />
        </div>
    );
}

本文标签: