csv-parse で TSV/CSV をパースするときに型変換を行う

# 前提
TSV ファイルを DB に insert するとき、 [csv-parse](https://csv.js.org/parse/) というモジュール[^1]を使ってパースを行いました。
ただその際、デフォルトでは数値や真偽値もキャストされず String として読み込まれてしまいます。 

# 方法
`cast` オプションにキャストを行うコールバック関数を書いて渡します。
コールバック関数は value と context を引数として受け取ります。
[context が持っている様々なプロパティ](https://csv.js.org/parse/options/cast/#context)を使って列を指定し、value をキャストして値を return します。
なお、今回は TSV ファイルを扱っていますが、 CSV ファイルについても同様です。

# 環境
Bun 1.1.3

### ファイル構成
```
|--index.ts
|--joyokanji.tsv
```
```joyokanji.tsv
id	grapheme_orth	grapheme_alt	grapheme_olds	radicals	stroke_count	grade	is_new	onyomis	kunyomis
1	亜		亞	二	7	S	false	ア	
2	哀			口	9	S	false	アイ	あわ-れ,あわ-れむ
3	挨			手	10	S	true	アイ	
4	愛			心	13	4	false	アイ	
5	曖			日	17	S	true	アイ	
6	悪		惡	心	11	3	false	アク,オ	わる-い
```
[https://ja.wikipedia.org/wiki/常用漢字一覧](https://ja.wikipedia.org/wiki/%E5%B8%B8%E7%94%A8%E6%BC%A2%E5%AD%97%E4%B8%80%E8%A6%A7) から取ってきて適当に加工したものです。

```sh:sh
bun add csv-parse
```
`npm install` とか `yarn add`とか。
忘れがちですがやっておきましょう。

# コード例
### 例0: オプションに cast を指定しない場合
```ts:index.ts
import fs from 'fs';
import { parse } from 'csv-parse/sync';

const file = fs.readFileSync('joyokanji.tsv', 'utf8');
const options = {
  columns: true,
  delimiter: '\t'
};
const records = parse(file, options);

console.log(records);
```

**実行結果**
```ts
[
  {
    id: "1",
    grapheme_orth: "亜",
    grapheme_alt: "",
    grapheme_olds: "亞",
    radicals: "二",
    stroke_count: "7",
    grade: "S",
    is_new: "false",
    onyomis: "ア",
    kunyomis: "",
  }, {
    id: "2",
    grapheme_orth: "哀",
    grapheme_alt: "",
    grapheme_olds: "",
    radicals: "口",
    stroke_count: "9",
    grade: "S",
    is_new: "false",
    onyomis: "アイ",
    kunyomis: "あわ-れ,あわ-れむ",
  }, {

    ...

    
  }, {
    id: "6",
    grapheme_orth: "悪",
    grapheme_alt: "",
    grapheme_olds: "惡",
    radicals: "心",
    stroke_count: "11",
    grade: "3",
    is_new: "false",
    onyomis: "アク,オ",
    kunyomis: "わる-い",
  }
]
```
value がすべて String になっています。

### 例1: 列名によって型変換を行う
今回対象としたファイルにはヘッダー列があります。
こういった場合、そもそもオプションで `columns: true` を指定する必要がありますが、さらに `cast` オプション内の `context.column` で列名を取得・検査することで、任意の列を任意の型にキャストできます。
今回は `id` と `stroke_count` 列をInteger型に、 `is_new` 列を Boolean型にしたいので、
`switch` 文を使って `context.column` を検査し、それぞれの `value` をキャストして `return` しています。
```ts.index.ts
import fs from 'fs';
import { parse } from 'csv-parse/sync';

const file = fs.readFileSync('joyokanji.tsv', 'utf8');
const options = {
  columns: true,
  delimiter: '\t',
  cast: function(value, context) {
    switch(context.column) {
      case 'id':
      case 'stroke_count':
        return parseInt(value, 10);
      case 'is_new':
        return value.toLowerCase() === 'true';
      default:
        return value;
    }
  }
};
const records = parse(file, options);

console.log(records);
```

**実行結果**
```ts
[
  {
    id: 1,
    grapheme_orth: "亜",
    grapheme_alt: "",
    grapheme_olds: "亞",
    radicals: "二",
    stroke_count: 7,
    grade: "S",
    is_new: false,
    onyomis: "ア",
    kunyomis: "",
  }, {
  
    ...
    
  }, {
    id: 5,
    grapheme_orth: "曖",
    grapheme_alt: "",
    grapheme_olds: "",
    radicals: "日",
    stroke_count: 17,
    grade: "S",
    is_new: true,
    onyomis: "アイ",
    kunyomis: "",
  }, {
    id: 6,
    grapheme_orth: "悪",
    grapheme_alt: "",
    grapheme_olds: "惡",
    radicals: "心",
    stroke_count: 11,
    grade: "3",
    is_new: false,
    onyomis: "アク,オ",
    kunyomis: "わる-い",
  }
]
```
キャストしてパースできました。

ちなみにヘッダー列がない場合は、例えば `context.index` プロパティで列インデックスを指定してキャストするといいと思います。

# 余談
みんな大好き [Papa Parse](https://www.papaparse.com) には `dynamicTyping` というオプションが用意されていて、これを `true` にしておくだけで、よしなに型変換を行ってくれるそうです。
モジュールの選定は大事ですね。
[^1]:csv-parse は [csv](https://csv.js.org/) モジュールにも含まれているので、 csv を install/add することでも利用できます。csv は csv-generate, csv-parse, stream-transform, そして csv-stringify の4つのモジュールのパッケージです。