james
james

Reputation: 131

How to handle number input in typescript?

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

Answers (5)

liquidki
liquidki

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

Zachary Haber
Zachary Haber

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/

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.

Upvotes: 15

Yilmaz
Yilmaz

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

Juraj Kubič&#225;r
Juraj Kubič&#225;r

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

Lo&#239;c Goyet
Lo&#239;c Goyet

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

Related Questions