30
votes

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.

6
onClick?: React.MouseEventHandler<HTMLElement> means onClick can be undefined. - tkausl
Because it's optional? - HalfWebDev
Remove the ? to make the onClick required. If it is optional it can by defintion be undefined - Titian Cernicova-Dragomir
Since I can't remove the ? from onClick, would adding defaultProps be a good idea along with typescript? Seems like I've to do it now. But I've read in many articles to handle defaultProps with TS only - HalfWebDev
It's still the same guys even after removing optional mark - HalfWebDev

6 Answers

17
votes

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>.

5
votes

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/

3
votes

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

1
votes

The best variant is to use ?.call(this: unknown, ...args: any[]) or ?.apply(this: unknown, args: any[]) methods

So, lets imagine we have next declarations

type callback = ((x: number, y: number) => number) | null;

let a: callback;
let b: callback;

a = (x, y) => x + y;   // it works with arrow functions
b = function (x, y) {  // and normal functions
  return x + y;
};

function x(cb1: callback, cb2: callback) {
  console.log(cb1?.call(0, 5, 6));     // in this case you
  console.log(cb2?.call(0, 5, 6));     // cant invoke cb1() or cb2()
  console.log(cb1?.apply(0, [5, 6]));  // but you can use call or apply
  console.log(cb2?.apply(0, [5, 6]));  // where first parameter can be any value
}

x(a, b); // 11 11 11 11

class C {
  public f?: callback;
  public setF() {
    this.f = (x, y) => {
      return x + y;
    };
  }
}
const c = new C(); // same with objects
c.setF();
console.log(c?.f?.call(c, 2, 3)); // 5

1
votes
 (props.onClick && props.onClick(e));
0
votes

For any one who come next. Another option is to use type casting. like:

props = props as NativeProps

In my experience I used a context who return a Partial type object and i needed to do type casting to overcome the undefined error. like:

const {setSomething} = useContext(SomePartialContext) as MyContextType