Typescript React: generic-based callback event handler function as prop to component - reactjs

I'm having trouble creating a generic-based callback event handler function that I want to pass as prop down to my component.
My goal: Allow user to pass a custom callback function that:
always takes in the same argument
an event (not a react/dom event handler event, its coming from a library)
can return different return type
for my use cases, this component is used in different context's so in one place a user will return a certain value over another
What I have Attempted
I have my onNodeClick function defined using generics & when i use it in isolation, it works.
// ✅ Simple example of calling a generic function
const onNodeClick = <T,> (event:any) => {
return null as unknown as T;
}
const string_result = onNodeClick<string>(event_from_somewhere)
However, when I try to pass this method as a prop to my component, I am getting errors. I'm unsure of how to resolve it
Live Typescript Code Playground
import React from 'react';
type NodeComponentProps = {
onNodeClick: <T>(event: any) => T;
};
export const NodeComponent = ({onNodeClick}: NodeComponentProps) => {
return null;
}
const Homepage = () => {
const handleNodeClick = <T,>(event: any): T => {
return null as unknown as T;
};
return (
<NodeComponent
onNodeClick={(event): string => {
const string_result = handleNodeClick<string>(event); // ✅ correct type
return string_result; // ❌ onNodeClick is throwing type error; see error message a few lines below
}}
/>
)
}
/*
Type '<T>(event: any) => string' is not assignable to type '<T>(event: any) => T'.
Type 'string' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'string
*/

I think you want the type to be generic, not the function. If the function is generic, then it should be callable with angle brackets and work correctly. For example, the identity function works with any type, so it should pass your generic function definition:
const Homepage = () => {
const onNodeClick = <T>(x: T): T => {
return x;
};
return (
<NodeComponent
onNodeClick={onNodeClick}
/>
)
}
However, the function you used will always return a string. If you tried to call it with <number>, it would fail. Therefore, not a generic function.
By contrast, if you make the type generic, you should be able to specify you are using it with a string:
Here is a Playground link
type NodeComponentProps<T> = {
onNodeClick: (event: any) => T;
};
export const NodeComponent = <T,>({onNodeClick}: NodeComponentProps<T>) => {
return null;
}

Related

Annotating Custom React Hook With TypeScript

I have a custom React Hook that watches for a click outside of a specific element. It works just fine, but I am having trouble making TypeScript happy in a few places.
App.js
import { useRef, useCallback } from "react";
import useClick from "./useClick";
export default function App() {
const asideRef = useRef(null);
const handleStuff = useCallback(() => {
console.log("a click outside of the sidebar occurred.");
}, []);
useClick(asideRef, handleStuff);
return (
<div className="App">
<aside ref={asideRef}>
<nav>
<ul></ul>
</nav>
</aside>
</div>
);
}
useClick.js
import React, { useEffect } from "react";
const useClick = (ref: React.MutableRefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: React.MouseEvent): void => {
if (ref.current && !ref.current.contains(e.target as Node)) {
cb();
}
};
document.addEventListener("click", checkClick);
return () => {
document.removeEventListener("click", checkClick);
};
}, [ref, cb]);
};
export default useClick;
The first problem area is in App.js, where the useClick hook is called. TypeScript complains about the first parameter passed to useClick and gives a message of:
Argument of type 'MutableRefObject<null>' is not assignable to parameter of type 'MutableRefObject<HTMLElement>'.
Type 'null' is not assignable to type 'HTMLElement'.ts(2345)
I know this has something to do with me setting the initial value of the ref to null, and the argument for the ref in useClick being annotated as React.MutableRefObject<HTMLElement>. I just don't know how to fix it.
The second problem area is in useClick.js, where the event listeners are added and removed. TypeScript seems to have a problem with my checkClick function. The error is so long that I have no choice but to show a photo of it below.
If anyone has any idea's on how to fix these two issues, so TypeScript will be happy, the help would be greatly appreciated.
Hi Dan: I'll start with what needs to change, then explain why below. (I also renamed your TypeScript file extensions to reflect their content.)
useClick.ts
Before:
// ...
const useClick = (ref: React.MutableRefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: React.MouseEvent): void => {
// ...
After:
// ...
const useClick = (ref: React.RefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: MouseEvent): void => {
// ...
App.tsx
Before:
export default function App() {
const asideRef = useRef(null);
After:
export default function App() {
const asideRef = useRef<HTMLElement>(null);
First, let's address the checkClick function: This function signature should be assignable to the EventListener type in lib.dom.d.ts:
interface EventListener {
(evt: Event): void;
}
Because your parameter is React.MouseEvent, which does not extend the native Event, type, it is incompatible. Just use the native MouseEvent type instead, since you're adding the listener to the document (which has nothing to do with React anyway.)
Next, let's look at the function signature for useClick:
You might want to reference the type definitions for useRef while reading this part:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5149827c1f97541dd69d950039b83ace68e119e6/types/react/index.d.ts#L1025-L1063
The first parameter is the only one that needs to change, and it needs to change from a MutableRefObject to a RefObject. The difference between these can be subtle, but there are some great resources you can read to learn more about them. Here's one, for example:
https://dev.to/wojciechmatuszewski/mutable-and-immutable-useref-semantics-with-react-typescript-30c9
When creating a ref that you pass to React to use for an element reference, you should provide null as the initial argument to useRef (which you did) and also provide a generic type annotation for the type of value it should hold (in this case it's HTMLElement). When using a ref for a DOM element, React expects that you will never mutate this ref since it's being controlled by React, so the type that is returned by useRef in this case is simply a RefObject. Therefore, that's what you should use for the first parameter of useClick.

function argument types when get a value from an HTML element on react

So Im just making a function that gets a value from an HTML element (LI tag). And this function will be called on an onClick event as well.
const getContents = (e: any) => {
const data = e.target.textContent.split('some-separator') //.. and so on
// other stuff
}
return (
<li onClick={(e) => getContents(e)}>{some date}</li>
)
for the meantime, I'm putting any as I don't know and can't narrow the type for now. What would be the correct type for this, and when I try to put a fancy type - typescript would scream
(property) Node.textContent: string | null - Object is possible 'null'
import {SyntheticEvent} from 'react'
const getContents = (e: SyntheticEvent) => {
// ...
}
// or more specific
const getContents = (e: SyntheticEvent<HTMLInputElement>) => {
e.currentTarget.value
}

Typescript: how to declare a type that includes all types extending a common type?

TLDR: Is there a way in Typescript to declare a type that encompasses all types that extend a given interface?
My specific problem
I am writing a custom React hook that encapsulates logic for deciding whether or not an element is moused over. It is modelled roughly after this hook. It exposes a ref that should be able to take any HTMLElement:
const ref = useRef<HTMLElement>(null);
The problem is, if I try to use this ref on any specific React element, I get an error telling me that this specific element is not quite HTMLElement. For example, if I use it with HTMLDivElement, I get this error: argument of type HTMLElement is not assignable to parameter of type HTMLDivElement.
Here's a simple repro case of the problem above in Typescript playground
Obviously, I wouldn't want to list types of all html elements in my hook. Given that HTMLDivElement extends the HTMLElement type, is there a way of declaring that the type that I am actually after is not strictly HTMLElement, but whatever extends HTMLElement?
React code example
source code of the hook
import { useRef, useState, useEffect } from 'react';
type UseHoverType = [React.RefObject<HTMLElement>, boolean];
export default function useHover(): UseHoverType {
const [isHovering, setIsHovering] = useState(false);
let isTouched = false;
const ref = useRef<HTMLElement>(null); // <-- What should the type be here?
const handleMouseEnter = () => {
if (!isTouched) {
setIsHovering(true);
}
isTouched = false;
};
const handleMouseLeave = () => {
setIsHovering(false);
};
const handleTouch = () => {
isTouched = true;
};
useEffect(() => {
const element = ref.current;
if (element) {
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
element.addEventListener('touchstart', handleTouch);
return () => {
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
element.removeEventListener('touchend', handleTouch);
};
}
}, [ref.current]);
return [ref, isHovering];
}
which produces type error if used like this:
import useHover from 'path-to-useHover';
const testFunction = () => {
const [hoverRef, isHovered] = useHover();
return (
<div
ref={hoverRef}
>
Stuff
</div>
);
}
Type error in example above will be:
Type 'RefObject<HTMLElement>' is not assignable to type 'string | RefObject<HTMLDivElement> | ((instance: HTMLDivElement | null) => void) | null | undefined'.
Type 'RefObject<HTMLElement>' is not assignable to type 'RefObject<HTMLDivElement>'.
Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.
I think you are mistaken about the direction of the assignment that fails. If you have an interface A, then the type that matches all subclasses of A is just called A. This way, HTMLElement (i.e. is assignable from) any HTML element, e.g. HTMLDivElement.
This means that if you have a bunch of functions, one of them accepts HTMLDivElement, another accepts HTMLLinkElement etc, then there is no real type that you can pass to all of them. It would mean you expect to have an element that is both a div and a link and more.
Edited based on your edits of the question:
If the code you have works fine, and your only problem is that it doesn't compile, then just make your useHover generic, like this:
type UseHoverType<T extends HTMLElement> = [React.RefObject<T>, boolean];
function useHover<T extends HTMLElement>(): UseHoverType<T> {
const ref = useRef<T>(null); // <-- What should the type be here?
...
And then:
const testFunction = () => {
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
Something like this will make your code compile fine, without changing its runtime behaviour. I'm unable to tell if the runtime behaviour right now is as desired.
It works as expected, since HTMLDivElement extends HTMLElement. In your typescirpt playground you mixed it up. I updated it by switching x and y in this playground. You want the function to extend HTMLElement and pass y, which is and HTMLDivElement into it. And that works.

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.
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/
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>.
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
I was creating my own custom MUI button and this is how I did it.
interface ButtonTypes {
handleClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
}
On the button component
<LoadingButton
onClick={(e) =>handleClick && handleClick(e)}
>
{"Send"}
</LoadingButton>
If you're using VScode, hover on the OnClick property and you should see the expected types.
The logical operator AND (&&) returns either the value of the first falsy operand it finds or the value of the last operand if all values are truthy (Source). Therefore, we can simply write:
(props.onClick && props.onClick(e));
The above solution is too much confusing I was getting the error
Cannot invoke an object which is possibly 'undefined
It can be easily solved by removing ? from MyProps
For Example
type CounterProps = {
value : number,
selected? : boolean,
onDelete? :(id : number) => void,
//remove ? from onDelete to solve the problem
id? : any,
}
// bottom line will shows Cannot invoke an object which is possibly undefined
<button onClick={() => this.props.onDelete(this.props.id)}>Delete</button>
A good explanation is provided here
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
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
This is the syntax that helped solve this issue for me.
Different ways of getting a solution:
search.prop('onSearch')!('text' as unknown)
search.prop('onSearch')!({} as unknown)
search.prop('onSearch')!({} as any)
The key portion is: !({} as any}

Flow: optional prop that uses generic types

I've got a react component that has a function prop called showModal. I've annotated this function elsewhere using generic types, but here, I want it to be an optional prop. Usually I do that with func?: () => void, but that syntax doesn't work in this case (I get Parsing error no matter where I place the ?):
type props = {
showModal<T>(T => React$Node, T): void,
}
How do I specify that showModal is optional? I've looked through the docs for Flow, but can't find anything relevant to this issue.
You need to tweak the function expression. Flow Try link
type Props = {
showModal?: <T>(T => React$Node, T) => void,
}
const foo: Props = {
showModal: (a, b) => undefined
}
const bar: Props = {
showModal: undefined
}

Resources