はじめに
どうもこんにちは 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ですね。
小数点の方も似たような現象でした。
そこで今回の対応としては次のようにしました。
文字列で受け取るようにして、文字列の正規表現でバリデートする方法です。
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"として対応したらうまく行った。
昔の誰か(もしかしたら自分が) ||と書き間違えたのかもしれない。
とにかくよかった。
終わりに
最近、本番落とすことが多くてなかなか神経質になっていたから
コードをより注意深くみる癖がついたことによる発見だと思う。
気を抜いていたわけではなかったが、これからも継続していきたい