42
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript or TypeScriptで大きい数字を扱うときは罠がいっぱい。

Posted at

はじめに

どうもこんにちは torihaziです。

今日はTypeScriptのnumberを扱っていた時に

少し詰まったのでその備忘録になります。

特に formで大きな数値を扱ったり、表示したりするときにハマる可能性大です。

今回はreact-hook-formとzodを組み合わせて使用した例を出しています。

罠1: numberは十分大きい数だと科学的記法になる

例えば 以下のような数値(整数10桁、小数点2桁まで)を入力するformとzodのスキーマがあったとします。

zodのスキーマ = z.object({
 value: z.number()
    .refine(
      (v) => {
        const [intrger, _] = v.toString().split(".");
        return integerPart.length <= 10;
      },
      { message: "整数部分は10桁以内で入力してください" }
    )
    .refine(
      (v) => {
        const [_, decimalPart] = v.toString().split(".");
        return !decimalPart || decimalPart.length <= 指定した桁;
      },
      { message: "小数点以下はxx桁以内で入力してください" }
    ),
    .....
~~~

const {register} = useForm<型>({
  mode: ~~,
  resolver: zodResolver(zodのスキーマ),
  values: {
    value: 0
    ...
  }
)};

~~ view ~~
<input 
    type="number"
    {...register("value"), {
        valueAsNumber: true
    }}
/>

input type=numberを使用しているformです。

ここに例えば 100000000000000000000 (1垓 = 0が20個)を入力します。

その数値までは確かに機能するのでzodのバリデーションも機能するのですが、

そこから0を1個増やすとこの数字は科学的記法に変換されます。

どういうことかというと

1000000000000000000000 (0が21個) => 1e+21

として扱われます。

これが起きるとどうなるかというと、

先ほどのzodのルールをpassします。それもそのはず。

1e+21を toStringした後に "."でsplitして その後lengthを測るのだから

結果的に 1e+21のlength = 5 <= 10 となるからです。

これ、useFormのwatchを使って0を書き足して行った時のlogですね。

スクリーンショット 2025-04-18 16.37.45.png

小数点の方も似たような現象でした。

そこで今回の対応としては次のようにしました。

文字列で受け取るようにして、文字列の正規表現でバリデートする方法です。

zodのスキーマ = z.object({
+ value: z.string()
+         .refine((v) => /^\d{1,13}(\.\d{1,2})?$/.test(v), {
+           message: "整数部分は10桁以内、小数部分は2桁以内"
+         })
+         .transform((v) => parseFloat(v)),
- value: z.number()
-    .refine(
-      (v) => {
-        const [intrger, _] = v.toString().split(".");
-        return integerPart.length <= 10;
-      },
-      { message: "整数部分は10桁以内で入力してください" }
-    )
-    .refine(
-      (v) => {
-        const [_, decimalPart] = v.toString().split(".");
-        return !decimalPart || decimalPart.length <= 指定した桁;
-      },
-      { message: "小数点以下はxx桁以内で入力してください" }
-    ),
    .....
~~~

const {register} = useForm<型>({
  mode: ~~,
  resolver: zodResolver(zodのスキーマ),
  values: {
    value: 0
    ...
  }
)};

~~ view ~~
<input 
    type="number"
+    {...register("value")}
-    {...register("value"), {
-        valueAsNumber: true
-    }}
/>

これで無事動作しました。

罠2: ビット演算子を使うと32ビット整数に変換される。

例えば以下のような計算式、出力はどうなると思いますか。

ちなみに数字は100億です。

console.log(10000000000|0)

答えは。

console.log(10000000000|0)
=> 1410065408

なぜでしょうか。

問題の鍵は |(ビット演算子(OR)) にあります。

オペランドは 32 ビットの整数値に変換され、ビット (ゼロまたは 1) の並びによって表現されます。32 ビットを超える数値は最上位のビットが破棄されます。例えば、次の 32 ビットを超える整数は 32 ビット整数に変換されます。

どういうことかというと

1. 10000000000|0 の時点で どちらも32ビットに変換する

2. 10000000000を32ビットに変換

3. しようとしたけど32ビットの最大値を超えてるから切り捨て。

4. 1001010100000010111110010000000000 (34ビット)なので先頭2つを切り捨て

5. 10を切って 01010100000010111110010000000000となる。

6. 残った方を計算すると 1410065408

7. 10000000000|0 => 1410065408 として出力。

ということ。

実際、データベースにはちゃんと100億が記録されていたので

よかったものの少し焦った。

解決策としてはビット演算子を使わなければいいだけのこと。

コード読んでみると "変数 | 0"みたいな書き方をされており、

おそらく undefinedとかだったら 0にするロジックだと判断したので

"変数 ?? 0"として対応したらうまく行った。

昔の誰か(もしかしたら自分が) ||と書き間違えたのかもしれない。

とにかくよかった。

終わりに

最近、本番落とすことが多くてなかなか神経質になっていたから

コードをより注意深くみる癖がついたことによる発見だと思う。

気を抜いていたわけではなかったが、これからも継続していきたい

42
30
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?