Reputation: 131
this is how a regular input field would be handled in react/typescript where value
is type string:
const [value, onChange] = useState<string>('');
const onChange = (e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value);
}
return (
<input
value={value}
onChange={onChange}
/>
);
But I'm having trouble where my input is of type number:
const [value, onChange] = useState<number>();
const onChange = (e: ChangeEvent<HTMLInputElement>) {
// type error
setValue(e.target.value);
}
return (
<input
type="number"
value={value}
onChange={onChange}
/>
);
in this case, value is of type number, input is of type number (so hypothetically e.target.value
should always be of type number. However according to typescript, event.target.value
is always a string. I can cast it to number ie.
const onChange = (e: ChangeEvent<HTMLInputElement>) {
setValue(Number(e.target.value));
}
but now if some number is typed in and the user backspaces it to make it empty, the value changes to 0 - the field can never be empty.
keeping value as string and then casting it to number on save is not a viable solution
something i'm temporarily doing is making e.target.value
as any:
const onChange = (e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value as any);
}
which works great but am I missing something to keep this type safe?
Upvotes: 13
Views: 27030
Reputation: 1304
The HTML input="number" spec for reference. From the spec:
The value sanitization algorithm is as follows: If the value of the element is not a valid floating-point number, then set it to the empty string instead.
I used this information to build a similar implementation, but testing value === ''
to know when the value didn't parse as a valid number.
Here's a full implementation in React/TS:
import React, { ChangeEventHandler, useState } from 'react';
// NumberInput component
interface INumberInput {
onChange?: ChangeEventHandler<HTMLInputElement> | undefined;
value: string | number | readonly string[] | undefined;
}
const NumberInput = ({ onChange, value }: INumberInput) => {
return (
<input
onChange={onChange}
type="number"
value={value ?? ''} // prevent React error "...switch from uncontrolled to controlled..."
/>
);
};
const TestNumberInput = () => {
// State
const [numberValue, setNumberValue] = useState<number>();
// onChange handler
const handleChangeNumber =
(setState: React.Dispatch<React.SetStateAction<number | undefined>>) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const valueAsNumber = event.target.valueAsNumber;
console.log('value:', value);
console.log('valueAsNumber:', valueAsNumber);
// A number input field, i.e. <input type="number" /> returns an empty string when the
// input does not parse as a number, so set the state to undefined in this case.
if (value === '') {
console.log('setting number value to undefined');
setState(undefined);
} else {
console.log('setting number value:', valueAsNumber);
setState(valueAsNumber);
}
};
return <NumberInput onChange={handleChangeNumber(setNumberValue)} value={numberValue} />;
};
export default TestNumberInput;
I prefer undefined
to null
, as the I find the state more readable, and the expected type of value on the HTML input component is string | number | readonly string[] | undefined
. I added the console.log()
statements so you can see what's happening as well as illustrate one caveat.
The caveat to both my answer and the isNaN
/ valueAsNumber
approach is that a number ending in a decimal is not a valid number. Thus, you'll see upon entering 12.
that value
will be ''
and valueAsNumber
will be isNaN
. Adding another number after the decimal will return the value to a number.
Upvotes: 0
Reputation: 11037
In general, you'll want to make your variable a string for everything to work well.
Then when you want to use it as a number, convert it at that point. You can easily convert it to a number using something like +value
or parseFloat(value)
The input uses a string type for a reason. And this way you keep the typesafety that typescript provides.
Many people suggest avoiding the number input altogether for various reasons: https://stackoverflow.blog/2022/12/26/why-the-number-input-is-the-worst-input/
If you want to keep the data for the number input as a number, you can use the suggestion from Loïc Goyet's answer:
const [value, onChange] = useState<number|null>(null);
const onNumberChange = (e: ChangeEvent<HTMLInputElement>) {
// In general, use Number.isNaN over global isNaN as isNaN will coerce the value to a number first
// which likely isn't desired
const value = !Number.isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : null;
setValue(value);
}
return (
<input
type="number"
value={value ?? ''}
onChange={onNumberChange}
/>
);
This works as it does specifically because number inputs are a bit weird in how they work. If the input is valid, then the value
and by extension valueAsNumber
will be defined, otherwise value
will be an empty string ''
.
You can use that with the nullish coalescing operator ??
to apply the empty string whenever you don't have a value, so that your state can be number|null
instead of number|string
or even number|undefined
if you set your values that way. It's important to use ??
instead of ||
as 0
is a falsy
value in JS! Otherwise you'll prevent the user from typing in 0
. This is similar to why the value kept changing to 0
for the OP. Number()
has some potentially unexpected behavior for some inputs, it appears to
treat most falsy values as either 0
or NaN
.
This will only work with the type="number"
inputs, due to the way the browser and react treats them. Doing it this way instead of the way above will break if the browser doesn't support type="number"
inputs, in which case it falls back to type="text"
.
You will also lose the information about how the user entered the data if that is relevant to you: e.g. 1e2
vs 100
.
Upvotes: 15
Reputation: 49709
When you set type="number"
, there are 3 things you should be aware of:
1- type of input will be string
. console.log(typeof event.target.value)
2- If you type a number less than 10, if you console.log(event.target.value
, it will log 01,02,03
3- you wont be able to delete 0 from the input. In order to add 11, you need to manually bring the cursor to the beginning of input and then type the value.
Solve using parseInt
//typescript will infer value as number
const [value, onChange] = useState(0);
inside onChange
const onChange = (e: ChangeEvent<HTMLInputElement>) {
// if you want decimal use parseFloat
// if you delete 0 and pass "" to parseInt you get NaN
const value = parseInt(e.target.value) || 0;
setValue(value);
}
to prevent the 3rd issue in input
element
<input
// if you see 0 just put "" instead
value={value || ""}
onChange={onChange}
type="number"
/>
Solve using Number
const [value, onChange] = useState(0);
in onChange
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
if (Number.isInteger(val) && val >= 0) {
setValue(val)
}
}
<input
// if you see 0 just put "" instead
value={value || ""}
onChange={onChange}
type="number"
/>
You could also set it as type string
but eventually if you needed to do some arithmetic operations, you have to use one of the above methods to convert it to a number
Upvotes: -1
Reputation: 1
I solved a similar problem, this is how I solved it.
const [value, setValue] = useState<string | number>('');
const handleChange = (e: ChangeEvent<HTMLInputElement>) {
setValue(+e.target.value);
}
return (
<input
type="number"
value={value === 0 ? '' : value}
onChange={handleChange}
/>
);
Upvotes: 0
Reputation: 750
You don't need to cast! You can simply reuse information held into the event!
The target
property from the event object has a valueAsNumber
property for every kind of inputs element which returns the value typed as number. Even if it's not a type="number"
, and so the valueAsNumber
equals NaN
.
Also from the target
has a type
property which will be equal to the HTML's type
attribute on your input target.
So combining those two properties, you could do :
const value = e.target.type === "number" && !isNaN(e.target.valueAsNumber) ? e.target.valueAsNumber : e.target.value
setValue(value);
Upvotes: 3