13

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?

5 Answers 5

15

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/

https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/


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.

Sign up to request clarification or add additional context in comments.

2 Comments

Would you say that all these values should be stored in the DB as strings too? So they're only converted in the application when it needs the number type (for example, to calculate something).
They shouldn't be. When you send the value to the backend, you should validate the results and ensure it's a valid number for your use case. You can do the parsing on the FE with a schema based validator like yup, zod, or others. You can and should still make sure the data is properly validated in the BE to prevent malicious inputs.
4

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);

Comments

0

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}
 />
);

Comments

0

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.

Comments

-1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.