How to properly type define event.target.value? - reactjs

I'm authoring a node package but I'm having bit of an issue with my typescript definitions. To be more specific I find the definition of event.target.value super confusing
Issue description:
I have the following event handler:
import { ChangeEvent, useState } from 'react'
type FieldEvent = ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
export const useField = <T>(input: T) => {
const [value, setValue] = useState<T>(input)
const handleChange = (event: FieldEvent) => {
const { name, value: eventValue } = event.target
// #ts-expect-error
setValue(eventValue)
}
return [input, handleChange]
}
The expression setValue(eventValue) results in the following error:
Argument of type 'string' is not assignable to parameter of type 'SetStateAction<T>'.
I was a bit surprised by this, given a lot of exported components use different event.target.value. Eg date-picker return Date type, select Object, etc.
Issue investigation
Naturally I went to check the imported ChangeEvent react exports to see if it has correct definitions, but this appears to be correct
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
target: EventTarget & T;
}
so according to this definition it should inherit the type of the Element that was passed to the SyntheticEvent
so I followed the chain to the HTMLInputElement declaration located in node_modules/typescript/lib/lib.dom.d.ts which is where the crux of the issue lies
interface HTMLInputElement extends HTMLElement {
value: string
//... rest
}
I checked back and it appears all the native <input> elements default to string as their value type, which I guess make sense.
Solving the issue
Obviously this is not ideal, given this does not represent the event.target.value behavior in a lot of the reactjs projects that use third-party-packages (which my package is supposed to support). Consider the following codesandbox
The returned event.target.value is as you'd expect of typeof number
that leads me to the question, should I simply override the ChangeEvent with the following definition?
ChangeEvent<{ value: T, name: string } & HTMLInputElement>
or would this be considered a bad practice? Or is there some better way to go about doing this altogether?

handleChange is not match to required params.
I've tried and it worked:
export default function App() {
const [selected, setSelected] = useState(1);
const handleChange = (e: ChangeEvent<{
name?: string | undefined,
value: unknown | number
}>, child: React.ReactNode) => {
setSelected(e.target.value as number);
};
return (
<Select value={selected} onChange={handleChange}>
<MenuItem value={1}>One</MenuItem>
<MenuItem value={2}>Two</MenuItem>
<MenuItem value={3}>Three</MenuItem>
</Select>
);
}

Alright, I'm not 100% sure if this is the correct approach but it seems to work fine for my use-case, albeit the typing seems a tiny bit odd, but basically I'm overwriting the passed type argument to ChangeEvent and extending it by a union of one the HTML elements.
export type FieldEvent<T> = ChangeEvent<
{ value: T, name?: string } &
(HTMLInputElement | HtmlTextAreaElement | HTMLSelectElement)
>
This overwrites the type definition of the ChangeEvent, then you just need to create a handler function that extends the type argument
export type FieldHanderFunction<T> = (event: FieldEvent<T>) => void
so then inside my hook, it basically comes down to:
const useField<T> = (input: T) => {
const handleChange = (event: FieldEvent<T>) => {
// ...
}
}

Related

React with Typescript

I'm using typescript with React. Whenever I pass props to function or destructure the props to {text} e.g. I get an error that says "Binding element 'text' implicitly has an 'any' type". Although I can bypass this with using const Example = ({text}:any) => { return () } or const Example = ({text}:{text:string}) => { return () }
it is not efficient for me. I wanted to ask if you can teach me any good way to overcome this without props argument itself but the destructured version of it.
Note: I see that I couldn't explain my problem well. I'm using Typescript because I want to use its type checking functionality. Giving every prop "any" type is ok but when I try to give every prop its own type e.g const Example = ({Text, Number, RandomCrap,}: {Text: string; Number: number; RandomCrap: any;}) => {return ();} It looks like this. Is this the only way?
Answer: Instead of cramming my code like above I can define types elsewhere like
type PropsType = {Text: string; Number: number; RandomCrap: any;};
and use it in my function like const Header = ({ Text, Number, RandomCrap }: PropsType) => {return ()}
Thanks everyone :)
You can define a type for your whole props object:
type PropsType = {
text: string
}
const Example = ({text}: PropsType) => { return () }
// or
const Example1: React.FunctionComponent<PropsType> = ({text}) => { return () }

React Hooks & Typescript - Forms not controlled?

I'm trying out Hooks and Typescript for the first time after having spent months using class components.
I'm trying to setup a search bar, and I can't for some reason get it to be a controlled controlled form.
I can't also set initial state as an empty string '' with useState.
event.target.value also returns an error of "Argument type string is not assignable to parameter of type.
I did go over other posts but I can't seem to get the forms controlled and the other issues resolved. Below are my code. I'd appreciate any input I could get.
import React, { FC, ChangeEvent, useState } from 'react';
interface SearchQueries {
searchTerm: string;
}
const Search: FC<SearchQueries> = () => {
const [searchTerm, setSearchTerm] = useState<{
searchTerm: string;
}>(''); **<-- Argument type of string is not assignable**
const updateSearchTerm = (event: ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
setSearchTerm(event.target.value);
};
return (
<input
type="text"
placeholder="Enter cocktail name"
// value={searchTerm} <-- tried commenting out to test if it's controlled (it's not)
onChange={updateSearchTerm}
/>
);
};
export default Search;
All you need for the input value is a string, so that's what you should use for the useState type parameter - or, even better, leave off the type parameter entirely and let TS infer it (inferring is usually preferable, since it requires less code and results in fewer mistakes like this one):
const [searchTerm, setSearchTerm] = useState('');
This will also result in setSearchTerm(event.target.value); working too.
If you wanted to pass down props and use them as the initial state (as your FC<SearchQueries> seems to indicate), then you need to declare the props in the argument list and use them:
const Search: FC<SearchQueries> = ({ searchTerm: initialSearchTerm }) => {
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);

Typescript: function with argument of enum type assigned to callback with argument type of string

I have a dropdown component with a callback onSelect:(option: string) => void
I'm using it as a sort picker in one application, where the options are of enum type:
enum SORT_OPTIONS {
LATEST = 'LATEST',
OLDEST = 'OLDEST'
}
const onSortSelect = (val: SORT_OPTIONS) => {...}
<MyDropdown onSelect={onSortSelect} ... />
And typescript complains:
Type '(val: SORT_OPTIONS) => void' is not assignable to type '(option: string) => void'.
Types of parameters 'val' and 'option' are incompatible.
Type 'string' is not assignable to type 'SORT_OPTIONS'. TS2322
It's a bit counter-intuitive but makes sense, since in MyDropdown a string option will be passed to the onSortSelect callback, which requires an enum value.
Question is what is the best way to fix the types here? IMO both functions are typed correctly, onSelect should accept any string since MyDropdown can be used in any context. onSortSelect should only accept SORT_OPTIONS.
Currently I'm casting the type of onSortSelect: <MyDropdown onSelect={onSortSelect as (val:string) => void} ... /> but it feels quite verbose
Your callback deals with onSelect:(option: string) => void
This means that it passes any string to the handle, like: hello, test, and so on.
But your const onSortSelect = (val: SORT_OPTIONS) => {...} expects only subset of strings: LATEST, OLDEST.
So, TypeScript warns you that you can't pass the whole set to a function that expects subset.
To fix the issue, you need to update your onSelect handler to:
onSelect:(option: SORT_OPTIONS) => void
I managed to fix the types with Generics, no typescript error
enum SORT_OPTIONS {
NEWEST='1',
OLDEST='2'
}
interface DropdownProps<T extends string> {
onSelect: (val: T) => void;
options: T[];
}
const Dropdown = <T extends string>(props: DropdownProps<T> & {children?: React.ReactNode}) =>{
const {onSelect, options} = props;
return <select onChange={(evt) => onSelect(evt.target.value as T)}>
{options.map(option => <option>{option}</option>)}
</select>
};
const TestComp:React.FC = () => {
const onSelect = (val: SORT_OPTIONS) => {
switch (val) {
case SORT_OPTIONS.OLDEST:
console.log('old');
break;
case SORT_OPTIONS.NEWEST:
console.log('new');
break;
}
};
const onSelect2 = (val: string) => {
console.log(val);
};
return <>
<Dropdown
options={[SORT_OPTIONS.NEWEST, SORT_OPTIONS.OLDEST]}
onSelect={onSelect}
/>
<Dropdown
options={['aaa', 'bbb']}
onSelect={onSelect2}
/>
</>
};
Note that
I can't type Dropdown with the regular React.FC, see the discussion here: How to use generics in props in React in a functional component?
I have to do a type casting to the Generic type.
Please comment if you find a different conclusion from the link.

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}

Resources