Skip to content

feat(csv-parse): add generic type argument #457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions packages/csv-parse/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import * as stream from "stream";

export type Callback = (
export type Callback<T = string[]> = (
err: CsvError | undefined,
records: any | undefined,
info: Info,
records: T[] | undefined,
info?: Info,
) => void;

export interface Parser extends stream.Transform {}
Expand Down Expand Up @@ -43,14 +43,19 @@ export type CastingDateFunction = (
context: CastingContext,
) => Date;

export type ColumnOption = string | undefined | null | false | { name: string };
export type ColumnOption<K = string> =
| K
| undefined
| null
| false
| { name: K };

/*
Note, could not `extends stream.TransformOptions` because encoding can be
BufferEncoding and undefined as well as null which is not defined in the
extended type.
*/
export interface Options {
export interface Options<T = string[]> {
/**
* If true, the parser will attempt to convert read data types to native types.
* @deprecated Use {@link cast}
Expand Down Expand Up @@ -84,7 +89,16 @@ export interface Options {
* default to null,
* affect the result data set in the sense that records will be objects instead of arrays.
*/
columns?: ColumnOption[] | boolean | ((record: any) => ColumnOption[]);
columns?:
| boolean
| ColumnOption<
T extends string[] ? string : T extends unknown ? string : keyof T
>[]
| ((
record: T,
) => ColumnOption<
T extends string[] ? string : T extends unknown ? string : keyof T
>[]);
/**
* Convert values into an array of values when columns are activated and
* when multiple columns of the same name are found.
Expand Down Expand Up @@ -149,8 +163,8 @@ export interface Options {
/**
* Alter and filter records by executing a user defined function.
*/
on_record?: (record: any, context: CastingContext) => any;
onRecord?: (record: any, context: CastingContext) => any;
on_record?: (record: T, context: CastingContext) => T | null | undefined;
onRecord?: (record: T, context: CastingContext) => T | null | undefined;
/**
* Optional character surrounding a field, one character only, defaults to double quotes.
*/
Expand Down Expand Up @@ -286,13 +300,28 @@ export class CsvError extends Error {
);
}

type OptionsWithColumns<T> = Omit<Options<T>, "columns"> & {
columns: Exclude<Options["columns"], undefined | false>;
};

declare function parse<T = unknown>(
input: string | Buffer,
options: OptionsWithColumns<T>,
callback?: Callback<T>,
): Parser;
declare function parse(
input: Buffer | string,
options?: Options,
input: string | Buffer,
options: Options,
callback?: Callback,
): Parser;
declare function parse(input: Buffer | string, callback?: Callback): Parser;
declare function parse(options?: Options, callback?: Callback): Parser;

declare function parse<T = unknown>(
options: OptionsWithColumns<T>,
callback?: Callback<T>,
): Parser;
declare function parse(options: Options, callback?: Callback): Parser;

declare function parse(input: string | Buffer, callback?: Callback): Parser;
declare function parse(callback?: Callback): Parser;

// export default parse;
Expand Down
12 changes: 11 additions & 1 deletion packages/csv-parse/lib/sync.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { Options } from "./index.js";

declare function parse(input: Buffer | string, options?: Options): any;
type OptionsWithColumns<T> = Omit<Options<T>, "columns"> & {
columns: Exclude<Options["columns"], undefined | false>;
};

declare function parse<T = unknown>(
input: Buffer | string,
options: OptionsWithColumns<T>,
): T[];
declare function parse(input: Buffer | string, options: Options): string[][];
declare function parse(input: Buffer | string): string[][];

// export default parse;
export { parse };

Expand Down
28 changes: 27 additions & 1 deletion packages/csv-parse/test/api.types.sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from "../lib/sync.js";

describe("API Types", function () {
type Person = { name: string; age: number };

it("respect parse signature", function () {
// No argument
parse("");
Expand All @@ -22,7 +24,7 @@ describe("API Types", function () {

it("return records", function () {
try {
const records: object = parse("");
const records = parse("");
typeof records;
} catch (err) {
if (err instanceof CsvError) {
Expand Down Expand Up @@ -96,4 +98,28 @@ describe("API Types", function () {
};
return info;
});

describe("Generic types", function () {
it("Exposes string[][] if columns is not specified", function () {
const data: string[][] = parse("", {});
});

it("Exposes string[][] if columns is falsy", function () {
const data: string[][] = parse("", {
columns: false,
});
});

it("Exposes unknown[] if columns is specified as boolean", function () {
const data: unknown[] = parse("", {
columns: true,
});
});

it("Exposes T[] if columns is specified", function () {
const data: Person[] = parse<Person>("", {
columns: true,
});
});
});
});
85 changes: 70 additions & 15 deletions packages/csv-parse/test/api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
import { parse as parse_sync } from "../lib/sync.js";

describe("API Types", function () {
type Person = { name: string; age: number };

describe("stream/callback API", function () {
it("respect parse signature", function () {
// No argument
Expand Down Expand Up @@ -89,16 +91,13 @@ describe("API Types", function () {
});

it("Receive Callback", function (next) {
parse(
"a\nb",
function (err: Error | undefined, records: object, info: Info) {
if (err !== undefined) {
records.should.eql([["a"], ["b"]]);
info.records.should.eql(2);
}
next(err);
},
);
parse("a\nb", function (err, records, info) {
if (err !== undefined) {
records!.should.eql([["a"], ["b"]]);
info!.records.should.eql(2);
}
next(err);
});
});
});

Expand Down Expand Up @@ -213,16 +212,28 @@ describe("API Types", function () {
false,
{ name: "column-name" },
];
options.columns = (record: string[]) => {
const fields: string[] = record.map((field: string) => {
options.columns = (record) => {
const fields = record.map((field: string) => {
return field.toUpperCase();
});
return fields;
};
options.columns = (record: string[]) => {
options.columns = (record) => {
record;
return ["string", undefined, null, false, { name: "column-name" }];
};

const typedOptions: Options<Person> = {};
typedOptions.columns = ["age", undefined, null, false, { name: "name" }];
typedOptions.columns = (record) => {
return ["age"];
};

const unknownTypedOptions: Options<unknown> = {};
unknownTypedOptions.columns = ["anything", undefined, null, false];
unknownTypedOptions.columns = (record) => {
return ["anything", undefined, null, false];
};
});

it("group_columns_by_name", function () {
Expand Down Expand Up @@ -289,8 +300,8 @@ describe("API Types", function () {

it("on_record", function () {
const options: Options = {};
options.on_record = (record, { lines }) => [lines, record[0]];
options.onRecord = (record, { lines }) => [lines, record[0]];
options.on_record = (record, { lines }) => [lines.toString(), record[0]];
options.onRecord = (record, { lines }) => [lines.toString(), record[0]];
});

it("quote", function () {
Expand Down Expand Up @@ -442,4 +453,48 @@ describe("API Types", function () {
});
});
});

describe("Generic types", function () {
it("Exposes string[][] if columns is not specified", function (next) {
parse("", {}, (error, records: string[][] | undefined) => {
next(error);
});
});

it("Exposes string[][] if columns is falsy", function (next) {
parse(
"",
{
columns: false,
},
(error, records: string[][] | undefined) => {
next(error);
},
);
});

it("Exposes unknown[] if columns is specified as boolean", function (next) {
parse(
"",
{
columns: true,
},
(error, records: unknown[] | undefined) => {
next(error);
},
);
});

it("Exposes T[] if columns is specified", function (next) {
parse<Person>(
"",
{
columns: true,
},
(error, records: Person[] | undefined) => {
next(error);
},
);
});
});
});