From 09aaf67331e907d1ccbe0e8281d76c7812db370d Mon Sep 17 00:00:00 2001 From: eunwoo song Date: Sun, 6 Sep 2026 14:18:39 +0900 Subject: [PATCH] fix(csv-parse): preserve typed column keys --- packages/csv-parse/lib/index.d.ts | 40 +++++++++++++---------- packages/csv-parse/test/api.types.sync.ts | 11 +++++++ packages/csv-parse/test/api.types.ts | 23 +++++++++++-- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/packages/csv-parse/lib/index.d.ts b/packages/csv-parse/lib/index.d.ts index 7119d61e..830553da 100644 --- a/packages/csv-parse/lib/index.d.ts +++ b/packages/csv-parse/lib/index.d.ts @@ -93,6 +93,15 @@ export type ColumnOption = | false | { name: K }; +type ColumnKey = T extends string[] + ? string + : unknown extends T + ? string + : string | keyof T; + +// Keep columns from overriding record types inferred from options such as raw. +type NoInferColumnRecord = [T][T extends unknown ? 0 : never]; + export interface OptionDelimiterAuto { preferred: Record; score: () => number; @@ -120,21 +129,17 @@ export interface OptionsNormalized { * Internal property string the function to */ cast_first_line_to_header?: ( - record: T, - ) => ColumnOption< - T extends string[] ? string : T extends unknown ? string : keyof T - >[]; + record: string[], + ) => ColumnOption>[]; /** * List of fields as an array, a user defined callback accepting the first * line and returning the column names or true if autodiscovered in the first * CSV line, default to null, affect the result data set in the sense that - * records will be objects instead of arrays. + * records will be objects instead of arrays. The callback receives the raw + * header fields as strings, while returned names may use keys from the typed + * input record. */ - columns: - | boolean - | ColumnOption< - T extends string[] ? string : T extends unknown ? string : keyof T - >[]; + columns: boolean | ColumnOption>[]; /** * Treat all the characters after this one as a comment, default to '' (disabled). */ @@ -305,15 +310,11 @@ export interface Options { * List of fields as an array, * a user defined callback accepting the first line and returning the column names or true if autodiscovered in the first CSV line, * default to null, - * affect the result data set in the sense that records will be objects instead of arrays. + * affect the result data set in the sense that records will be objects instead of arrays. The callback receives the raw header fields as strings, while returned names may use keys from the typed input record. */ columns?: - | OptionsNormalized["columns"] - | (( - record: T, - ) => ColumnOption< - T extends string[] ? string : T extends unknown ? string : keyof T - >[]); + | OptionsNormalized["columns"] + | ((record: string[]) => ColumnOption>[]); /** * Treat all the characters after this one as a comment, default to '' (disabled). */ @@ -510,7 +511,10 @@ export class CsvError extends Error { } export type OptionsWithColumns = Omit, "columns"> & { - columns: Exclude; + columns: Exclude< + Options, NoInferColumnRecord>["columns"], + undefined | false + >; }; declare function parse( diff --git a/packages/csv-parse/test/api.types.sync.ts b/packages/csv-parse/test/api.types.sync.ts index 0ae54251..91a17d29 100644 --- a/packages/csv-parse/test/api.types.sync.ts +++ b/packages/csv-parse/test/api.types.sync.ts @@ -125,5 +125,16 @@ describe("API Types", function () { }); data; }); + + it("Accepts typed keys from a columns callback", function () { + const columnMapping = new Map([ + ["full_name", "name"], + ["years", "age"], + ]); + const data: Person[] = parse("full_name,years\nAda,36", { + columns: (header) => header.map((column) => columnMapping.get(column)), + }); + data; + }); }); }); diff --git a/packages/csv-parse/test/api.types.ts b/packages/csv-parse/test/api.types.ts index 1f9276d8..521d565b 100644 --- a/packages/csv-parse/test/api.types.ts +++ b/packages/csv-parse/test/api.types.ts @@ -223,8 +223,9 @@ describe("API Types", function () { const typedOptions: Options = {}; typedOptions.columns = ["age", undefined, null, false, { name: "name" }]; - typedOptions.columns = (record: Person) => { - record; + typedOptions.columns = (record) => { + const header: string[] = record; + header; return ["age"]; }; @@ -501,6 +502,24 @@ describe("API Types", function () { ); }); + it("Accepts typed keys from a columns callback", function (next) { + const columnMapping = new Map([ + ["full_name", "name"], + ["years", "age"], + ]); + parse( + "full_name,years\nAda,36", + { + columns: (header) => + header.map((column) => columnMapping.get(column)), + }, + (error, records: Person[]) => { + records; + next(error); + }, + ); + }); + it("Exposes U[] and T if columns and on_record are specified", function (next) { type PersonOriginal = { surname: string; age: number }; parse(