#Can yomeone help me to type this react hook

26 messages · Page 1 of 1 (latest)

carmine granite
#

Hello I have written a custom hook which pulls out the state management for a dialog. So basically it works like this:

const DialogHookExample = () => {
  const ButtonComponent = useDialog(Button, DialogContent);
  // ButtonComponent should
  // have combined props of Button and DialogContent
  return (
    <div>
      <ButtonComponent
        dialogContentProps={{
          header: 'Upload a file to import settings',
          subheader: 'upload testing exists'
        }}
        variant="secondary" // should be spread onto DialogActionComponent
      >
        import
      </ButtonComponent>
    </div>
  );
};

So you provide a component onto which you want to bind the onClick and you provide the Content of the dialog. You get back a component which accepts on the root level the props of the DialogActionComponent (the Button) and accept the props for the dialogContent as object.
The code works fine, however I am trying to type the hook generically so that the return component consists of the inferred types.

#

This is the implementation so far:

type TDialogContent = { onClick: () => void };

type TPropsOfReturnComponent<T> = {
  dialogContentProps?: TDialogContent;
} & T;

function useDialog<TAction extends PropsWithChildren, TContent>(
  DialogActionComponent: FC<TAction>,
  DialogContent: FC<TContent>
) {
  const ButtonComponent: FC<TPropsOfReturnComponent<TAction>> = (props) => {
    const [visible, setVisible] = useState<boolean>(false);
    const { dialogContentProps, children, ...rest } = props;
    return (
      <>
        <DialogActionComponent {...rest}>{children}</DialogActionComponent>
        {visible && (
          <Portal>
            <DialogContainer
              visible={visible}
              onHide={() => setVisible(false)}
            >
              <DialogContent
                {...dialogContentProps}
                close={() => setVisible(false)}
              />
            </DialogContainer>
          </Portal>
        )}
      </>
    );
  };

  return ButtonComponent;
}

The error I am seeing on DialogActionComponent are:

Type 'Omit<TPropsOfReturnComponent<TAction>, "children" | "dialogContentProps"> & { children: ReactNode; }' is not assignable to type 'IntrinsicAttributes & TAction'.
  Type 'Omit<TPropsOfReturnComponent<TAction>, "children" | "dialogContentProps"> & { children: ReactNode; }' is not assignable to type 'TAction'.
    'Omit<TPropsOfReturnComponent<TAction>, "children" | "dialogContentProps"> & { children: ReactNode; }' is assignable to the constraint of type 'TAction', but 'TAction' could be instantiated with a different subtype of constraint '{ children?: ReactNode; }'.ts(2322)
steady steeple
#

are excess props a problem in react? if not i think you could write this:

<DialogActionComponent {...props}>{children}</DialogActionComponent>
#

here's an example based on what you shared (it's missing some of the components though):

delicate thunderBOT
#
mkantor#7432

Preview:```ts
import React, {
PropsWithChildren,
FC,
useState,
} from "react"

type TDialogContent = {onClick: () => void}

type TPropsOfReturnComponent<T> = {
dialogContentProps?: TDialogContent
} & T

function useDialog<
TAction extends PropsWithChildren,
TContent

(
DialogActionComponent:
...```

viral cloak
#

That's because Omit<X,'Y'> & Pick<X,'Y'> is not equal to X
When you destructure something, typescript doesnt really know about all fields .

Have you tried

<DialogActionComponent props={props}/>

?

carmine granite
#
function useDialog<TDACP, TContent>(
  DialogActionComponent: FC<TDialogActionComponentProps<TDACP>>,
  DialogContent: FC<TContent>
) {
  const ButtonComponent: FC<{ dialogContentProps?: TDialogContent } & TDialogActionComponentProps<TDACP>> = (props) => {
    const [visible, setVisible] = useState<boolean>(false);
    const { dialogContentProps, ...rest } = props;
    const dialogActionComponentProps = rest as TDialogActionComponentProps<TDACP>;
    return (
      <>
        <DialogActionComponent
          {...dialogActionComponentProps}
          onClick={() => setVisible(true)}
        >
          {props.children}
        </DialogActionComponent>
        {visible && (
          <Portal>
            <DialogContainer
              visible={visible}
              onHide={() => setVisible(false)}
            >
              {/* <DialogContent
                {...props}
                close={() => setVisible(false)}
              /> */}
            </DialogContainer>
          </Portal>
        )}
      </>
    );
  };

Something like this

steady steeple
#

that's not safe. TS is warning you about a real problem

#

people are allowed to call useDialog like this:

useDialog<{ dialogContentProps: string }, Whatever>(...)
#

but you're dropping the dialogContentProps property internally, so DialogActionComponent isn't actually receiving a valid TAction ({ dialogContentProps: string }) in that case with your current implementation

#

it would not be clean to simply pass all props
how come?

carmine granite
#

Okay hang on now it is working like I want it to - but you saying this solution is not save because people could use it wrongly?

type TDialogContentProps<TContent> = TContent & {
  closeDialog: () => void;
};

type TDialogActionComponentProps<TDACP, TContent> = {
  dialogProps: {
    dialogContainerProps?: Omit<TDialogContainer, 'onHide'>;
    dialogContentProps?: Omit<TDialogContentProps<TContent>, 'closeDialog'>;
  };
  children?: ReactNode;
} & TDACP;

function useDialog<TDACP, TContent>(DialogActionComponent: FC<TDACP>, DialogContent: FC<TContent>) {
  const ButtonComponent: FC<TDialogActionComponentProps<TDACP, TContent>> = (props) => {
    const [visible, setVisible] = useState<boolean>(false);
    const { dialogProps, children, ...rest } = props;
    const dialogActionComponentProps = rest as TDACP;
    const dialogContentProps = dialogProps.dialogContentProps as TDialogContentProps<TContent>;
    const dialogContainerProps = dialogProps.dialogContainerProps as TDialogContainer;
    return (
      <>
        <DialogActionComponent
          {...dialogActionComponentProps}
          onClick={() => setVisible(true)}
        >
          {children}
        </DialogActionComponent>
        {visible && (
          <Portal>
            <DialogContainer
              {...dialogContainerProps}
              visible={visible}
              onHide={() => setVisible(false)}
            >
              <DialogContent
                {...dialogContentProps}
                closeDialog={() => setVisible(false)}
              />
            </DialogContainer>
          </Portal>
        )}
      </>
    );
  };

  return ButtonComponent;
}
steady steeple
#

yes. that's very often the case when you resort to type assertions (as)

carmine granite
#

so is there a way to entforce this and not result to the type conversion - I am feeling pretty stuiped do I not understand your suggestion?

#

SO you say I should simply spread the props downwards and do not cast

#

Why would this be better? then it could still be used wrongly couldn't it?

steady steeple
#

can you share what TDialogContainer is? i'll give you an example of why it is unsafe

carmine granite
#
import type { DialogProps } from 'primereact/dialog';

export type TDialogContainer = { dismissableMask?: boolean; draggable?: boolean; subheader?: string } & DialogProps;
steady steeple
#

okay so here's a simplified version of your last code sample with an example of usage that would be unsafe:

delicate thunderBOT
#
mkantor#7432

Preview:```ts
import React, {FC, useState, ReactNode} from "react"

type DialogProps = {} // just a stub
type TDialogContainer = {
dismissableMask?: boolean
draggable?: boolean
subheader?: string
} & DialogProps

type TDialogContentProps<TContent> = TContent & {
closeDialog: () => voi
...```

steady steeple
#

Component would throw an exception there when used in a dialog, because MyThingy is expecting there to be a prop named dialogProps, but that won't get passed through since you're destructuring it away

#

forwarding all props to the component would make it so that can't happen, which is why it's safer

carmine granite
#

ah u mean in case the button would accept props of the same name as I have typed here:

type TDialogActionComponentProps<TDACP, TContent> = {
  dialogProps: {
    dialogContainerProps?: Omit<TDialogContainer, 'onHide'>;
    dialogContentProps: Omit<TDialogContentProps<TContent>, 'closeDialog'>;
  };
} & TDACP &
  PropsWithChildren;
steady steeple
#

yes, dialogProps specifically is the problem