csv ファイルを型付きのオブジェクトとして読み込みたい。
例えば、以下のような csv ファイルであれば {name: string, age: string, gender: string}[]
型として読み込みたい。
name,age,gender
aaa,18,M
bbb,23,F
ccc,30,M
csv の読み込みは papaparse を利用しています。
import papa from 'papaparse'
const readCSV = <T extends {}>(file: File, config: papa.ParseConfig) =>
new Promise<papa.ParseResult<T>>(((resolve, reject) => {
config.complete = ((results: papa.ParseResult<T>, _) => resolve(results))
config.error = ((error, _) => reject(error))
papa.parse(file, config)
}))
// readonly ["name, "age"]
const required = ["name", "age"] as const
// Row<readonly ["name", "age"]> は {name: string, age: string}
type Row<T extends readonly string[]> = {[K in T[number]]: string}
// If header is true, the first row of parsed data will be interpreted as field names.
// An array of field names will be returned in meta, and each row of data will be an object of values keyed by field name instead of a simple array.
readCSV<Row<typeof required>>(file, {header: true})
.then((results) =>
required.every(e => results.meta.fields && results.meta.fields.includes(e))
? results.data // {name: string, age: string}
: undefined
)
パースされたオブジェクトが期待される型であることはランタイム時にはじめて分かります。(ランタイム時に型はないので、「分かる」という表現は不適かもしれません。)
各行の値がいずれも string
型であることは自明ですが、オブジェクトのキーが期待される名前であることはコンパイル時には分かりません。
そこで、required.every(e => results.meta.fields && results.meta.fields.includes(e))
という処理によって、オブジェクトに期待される名前のキーが存在するかを検証しています。papaparse はオブジェクトのキーを results.meta.fields
に格納するため、required
配列のすべての文字列が result.meta.fields
に含まれることを確認できます。
results.data
は required
配列で指定したキーをもつオブジェクトとして扱うことができます。つまり、そのオブジェクトの型は required
配列の型 ( readonly ["name, "age"]
) を T
とすると {[K in T[number]]: string}
になります。