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

# はじめに
どうもこんにちは **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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2567812/126ead13-e501-4c3b-99cd-43e1fb354032.png)

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

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

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

```diff
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 ビット整数に変換されます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Bitwise_OR

どういうことかというと

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

2. 10000000000を32ビットに変換

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

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

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

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

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

ということ。

https://hogehoge.tk/tool/number.html

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

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

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

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

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

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

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

とにかくよかった。

# 終わりに

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

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

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