Cannot invoke an object which is possibly 'undefined'.ts(2722)

I have a button component. I simply pass it just one onClick prop out of many optional props I've defined:

const Button = (props: ButtonProps) => {
    const handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => {
        props.onClick(e);
    }
    return (
        <StyledButton onClick={handleClick}>
            {props.children}
        </StyledButton>
    );
};

Then I'm using it like this:

<Button onClick={(e) => {
    console.log(e);
}}>Click me!</Button>

Now how can as per the error mentioned in question, object be possibly undefined? I'm clearly passing the function to it and that too as per the type definition. So, I'm passing an object to it. Simple enough!

...
onClick?: React.MouseEventHandler<HTMLElement>
...

I've added a few more strict checks in this project recently and relevant one's are:

"strictFunctionTypes": true,
"strictNullChecks": true

strict:true being already present, this error never occurred.

What's the issue here?

Update - Types added

export interface IBaseButtonProps {
    type?: ButtonType;
    disabled?: boolean;
    size?: ButtonSize;
    block?: boolean;
    loading?: boolean | { delay?: number };
    icon?: string;
    className?: string;
    prefixCls?: string;
    children?: React.ReactNode;
}

export type AnchorButtonProps = {
    href: string,
    target?: string,
    onClick: React.MouseEventHandler<HTMLElement>
} & IBaseButtonProps & Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;


export type NativeButtonProps = {
    onClick: React.MouseEventHandler<HTMLElement>,
    htmlType?: ButtonHTMLType
} & IBaseButtonProps & Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;

export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>

Notes:

The possible solution is to either destructure the props and add the default prop. Or use defaultProps from React. But not sure if I should require that really with Typescript.


Solution 1:

Now how can as per the erro mentioned in question, object be possibly undefined? [sic]

The use of Partial<T> around export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps> causes onClick to be optional. When we use Partial<T>, all the properties receive the ? and thus become optional, which means that all of them can be undefined.

There are two approached to a fix: one is to keep ButtonProps the same with onClick as optional, and to check that onClick is defined before calling it (fix 1); the other is to change ButtonProps to make onClick required (fix 2 and 3).

Fix 1: onClick remains optional

Use the ButtonProps that you already have, and then check that onClick is defined before calling it. This is what antd does in the code you linked in the comments.

const Button = (props: ButtonProps) => {
  const handleClick: React.MouseEventHandler<
    HTMLButtonElement | HTMLAnchorElement
  > = e => {
    if (props.onClick) props.onClick(e); // works
  };
};

Fix 2: onClick becomes required

Change ButtonProps by not applying the Partial to the NativeButtonProps:

type ButtonProps1 = Partial<AnchorButtonProps> & NativeButtonProps;

const Button1 = (props: ButtonProps1) => {
  const handleClick: React.MouseEventHandler<
    HTMLButtonElement | HTMLAnchorElement
  > = e => {
    props.onClick(e); // works
  };
};

Fix 3: onClick becomes required too

Define a RequireKeys type, which lets you to specify the keys that are not optional.

type RequireKeys<T, TNames extends keyof T> = T &
  { [P in keyof T]-?: P extends TNames ? T[P] : never };

type ButtonProps2 = RequireKeys<ButtonProps, "onClick">;

const Button2 = (props: ButtonProps2) => {
  const handleClick: React.MouseEventHandler<
    HTMLButtonElement | HTMLAnchorElement
  > = e => {
    props.onClick(e); // works
  };
};

The answers to Mapped Types: removing optional modifier have more information about how I defined RequireKeys<T>.

Solution 2:

With Typescript 3.7+, you can also use optional chaining to invoke the optional prop method:

const Button = (props: ButtonProps) => {
  const handleClick: React.MouseEventHandler<
    HTMLButtonElement | HTMLAnchorElement
  > = e => {
    props.onClick?.(e); // works
  };
};

You can read more about using optional chaining - https://www.stefanjudis.com/today-i-learned/optional-chaining-helps-to-avoid-undefined-is-not-a-function-exceptions/

Solution 3:

Just a clear cut answer

if (props.onClick) props.onClick(e);

if you are defining a function props and want it to be optional, define it as,

export type ButtonProps = {
  function?: () => void;
};

Explanation: If you want to use a function as props, there may be instances when you want to pass that function (as props) and there may be other instances where you don't want to pass it.

for example,

Common Code WHERE calling the <Home/> component, say index.ts/index.js

function myfunction(){
  //do something
  alert("hello")
}

return (
  <>
     <Home myfunction={myfunction}/>    //passing prop
     <Home/>                            // not passing
  </>
)

In JS, home.js

export default function Home({myfunction}) {
  const const1 = "Hello World"
  return (
    //do something
    myfunction();      //IMPORTANT line
  )
}

Now, its almost equivalent in TS, home.ts

In TS, we define types of everything. So, in that case we have to define type of this function myfunction also, that we are passing.

So, for this function, we realise that,

  • It recieves no params, so () (empty parenthesis) is enough, if any params are there, we need to define types for them also.
  • Returns nothing, so return type void
export type HomeProps = {
  myfunction?: () => void;
};

export default function Home({ myfunction }: HomeProps) {
  const const1 = "Hello World"
  return (
    //do something
    if (myfunction) myfunction();      //IMPORTANT line
  )
}

Hint: above answer