Skip to content

Commit ef71342

Browse files
authored
feat(csv-parse): add generic type argument (#457)
This is based on the columns option. When falsy, the records are string[][]. When defined, the records are T[], T defaulting to unknown. Restrict columns to keyof T. Update onRecord / on_record to take and return type T. Fix #278 Fix #407
1 parent 6457a90 commit ef71342

File tree

4 files changed

+149
-29
lines changed

4 files changed

+149
-29
lines changed

packages/csv-parse/lib/index.d.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
import * as stream from "stream";
66

7-
export type Callback = (
7+
export type Callback<T = string[]> = (
88
err: CsvError | undefined,
9-
records: any | undefined,
10-
info: Info,
9+
records: T[] | undefined,
10+
info?: Info,
1111
) => void;
1212

1313
export interface Parser extends stream.Transform {}
@@ -43,14 +43,19 @@ export type CastingDateFunction = (
4343
context: CastingContext,
4444
) => Date;
4545

46-
export type ColumnOption = string | undefined | null | false | { name: string };
46+
export type ColumnOption<K = string> =
47+
| K
48+
| undefined
49+
| null
50+
| false
51+
| { name: K };
4752

4853
/*
4954
Note, could not `extends stream.TransformOptions` because encoding can be
5055
BufferEncoding and undefined as well as null which is not defined in the
5156
extended type.
5257
*/
53-
export interface Options {
58+
export interface Options<T = string[]> {
5459
/**
5560
* If true, the parser will attempt to convert read data types to native types.
5661
* @deprecated Use {@link cast}
@@ -84,7 +89,16 @@ export interface Options {
8489
* default to null,
8590
* affect the result data set in the sense that records will be objects instead of arrays.
8691
*/
87-
columns?: ColumnOption[] | boolean | ((record: any) => ColumnOption[]);
92+
columns?:
93+
| boolean
94+
| ColumnOption<
95+
T extends string[] ? string : T extends unknown ? string : keyof T
96+
>[]
97+
| ((
98+
record: T,
99+
) => ColumnOption<
100+
T extends string[] ? string : T extends unknown ? string : keyof T
101+
>[]);
88102
/**
89103
* Convert values into an array of values when columns are activated and
90104
* when multiple columns of the same name are found.
@@ -149,8 +163,8 @@ export interface Options {
149163
/**
150164
* Alter and filter records by executing a user defined function.
151165
*/
152-
on_record?: (record: any, context: CastingContext) => any;
153-
onRecord?: (record: any, context: CastingContext) => any;
166+
on_record?: (record: T, context: CastingContext) => T | null | undefined;
167+
onRecord?: (record: T, context: CastingContext) => T | null | undefined;
154168
/**
155169
* Optional character surrounding a field, one character only, defaults to double quotes.
156170
*/
@@ -286,13 +300,28 @@ export class CsvError extends Error {
286300
);
287301
}
288302

303+
type OptionsWithColumns<T> = Omit<Options<T>, "columns"> & {
304+
columns: Exclude<Options["columns"], undefined | false>;
305+
};
306+
307+
declare function parse<T = unknown>(
308+
input: string | Buffer,
309+
options: OptionsWithColumns<T>,
310+
callback?: Callback<T>,
311+
): Parser;
289312
declare function parse(
290-
input: Buffer | string,
291-
options?: Options,
313+
input: string | Buffer,
314+
options: Options,
292315
callback?: Callback,
293316
): Parser;
294-
declare function parse(input: Buffer | string, callback?: Callback): Parser;
295-
declare function parse(options?: Options, callback?: Callback): Parser;
317+
318+
declare function parse<T = unknown>(
319+
options: OptionsWithColumns<T>,
320+
callback?: Callback<T>,
321+
): Parser;
322+
declare function parse(options: Options, callback?: Callback): Parser;
323+
324+
declare function parse(input: string | Buffer, callback?: Callback): Parser;
296325
declare function parse(callback?: Callback): Parser;
297326

298327
// export default parse;

packages/csv-parse/lib/sync.d.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { Options } from "./index.js";
22

3-
declare function parse(input: Buffer | string, options?: Options): any;
3+
type OptionsWithColumns<T> = Omit<Options<T>, "columns"> & {
4+
columns: Exclude<Options["columns"], undefined | false>;
5+
};
6+
7+
declare function parse<T = unknown>(
8+
input: Buffer | string,
9+
options: OptionsWithColumns<T>,
10+
): T[];
11+
declare function parse(input: Buffer | string, options: Options): string[][];
12+
declare function parse(input: Buffer | string): string[][];
13+
414
// export default parse;
515
export { parse };
616

packages/csv-parse/test/api.types.sync.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from "../lib/sync.js";
1313

1414
describe("API Types", function () {
15+
type Person = { name: string; age: number };
16+
1517
it("respect parse signature", function () {
1618
// No argument
1719
parse("");
@@ -22,7 +24,7 @@ describe("API Types", function () {
2224

2325
it("return records", function () {
2426
try {
25-
const records: object = parse("");
27+
const records = parse("");
2628
typeof records;
2729
} catch (err) {
2830
if (err instanceof CsvError) {
@@ -96,4 +98,28 @@ describe("API Types", function () {
9698
};
9799
return info;
98100
});
101+
102+
describe("Generic types", function () {
103+
it("Exposes string[][] if columns is not specified", function () {
104+
const data: string[][] = parse("", {});
105+
});
106+
107+
it("Exposes string[][] if columns is falsy", function () {
108+
const data: string[][] = parse("", {
109+
columns: false,
110+
});
111+
});
112+
113+
it("Exposes unknown[] if columns is specified as boolean", function () {
114+
const data: unknown[] = parse("", {
115+
columns: true,
116+
});
117+
});
118+
119+
it("Exposes T[] if columns is specified", function () {
120+
const data: Person[] = parse<Person>("", {
121+
columns: true,
122+
});
123+
});
124+
});
99125
});

packages/csv-parse/test/api.types.ts

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { parse as parse_sync } from "../lib/sync.js";
1111

1212
describe("API Types", function () {
13+
type Person = { name: string; age: number };
14+
1315
describe("stream/callback API", function () {
1416
it("respect parse signature", function () {
1517
// No argument
@@ -89,16 +91,13 @@ describe("API Types", function () {
8991
});
9092

9193
it("Receive Callback", function (next) {
92-
parse(
93-
"a\nb",
94-
function (err: Error | undefined, records: object, info: Info) {
95-
if (err !== undefined) {
96-
records.should.eql([["a"], ["b"]]);
97-
info.records.should.eql(2);
98-
}
99-
next(err);
100-
},
101-
);
94+
parse("a\nb", function (err, records, info) {
95+
if (err !== undefined) {
96+
records!.should.eql([["a"], ["b"]]);
97+
info!.records.should.eql(2);
98+
}
99+
next(err);
100+
});
102101
});
103102
});
104103

@@ -213,16 +212,28 @@ describe("API Types", function () {
213212
false,
214213
{ name: "column-name" },
215214
];
216-
options.columns = (record: string[]) => {
217-
const fields: string[] = record.map((field: string) => {
215+
options.columns = (record) => {
216+
const fields = record.map((field: string) => {
218217
return field.toUpperCase();
219218
});
220219
return fields;
221220
};
222-
options.columns = (record: string[]) => {
221+
options.columns = (record) => {
223222
record;
224223
return ["string", undefined, null, false, { name: "column-name" }];
225224
};
225+
226+
const typedOptions: Options<Person> = {};
227+
typedOptions.columns = ["age", undefined, null, false, { name: "name" }];
228+
typedOptions.columns = (record) => {
229+
return ["age"];
230+
};
231+
232+
const unknownTypedOptions: Options<unknown> = {};
233+
unknownTypedOptions.columns = ["anything", undefined, null, false];
234+
unknownTypedOptions.columns = (record) => {
235+
return ["anything", undefined, null, false];
236+
};
226237
});
227238

228239
it("group_columns_by_name", function () {
@@ -289,8 +300,8 @@ describe("API Types", function () {
289300

290301
it("on_record", function () {
291302
const options: Options = {};
292-
options.on_record = (record, { lines }) => [lines, record[0]];
293-
options.onRecord = (record, { lines }) => [lines, record[0]];
303+
options.on_record = (record, { lines }) => [lines.toString(), record[0]];
304+
options.onRecord = (record, { lines }) => [lines.toString(), record[0]];
294305
});
295306

296307
it("quote", function () {
@@ -442,4 +453,48 @@ describe("API Types", function () {
442453
});
443454
});
444455
});
456+
457+
describe("Generic types", function () {
458+
it("Exposes string[][] if columns is not specified", function (next) {
459+
parse("", {}, (error, records: string[][] | undefined) => {
460+
next(error);
461+
});
462+
});
463+
464+
it("Exposes string[][] if columns is falsy", function (next) {
465+
parse(
466+
"",
467+
{
468+
columns: false,
469+
},
470+
(error, records: string[][] | undefined) => {
471+
next(error);
472+
},
473+
);
474+
});
475+
476+
it("Exposes unknown[] if columns is specified as boolean", function (next) {
477+
parse(
478+
"",
479+
{
480+
columns: true,
481+
},
482+
(error, records: unknown[] | undefined) => {
483+
next(error);
484+
},
485+
);
486+
});
487+
488+
it("Exposes T[] if columns is specified", function (next) {
489+
parse<Person>(
490+
"",
491+
{
492+
columns: true,
493+
},
494+
(error, records: Person[] | undefined) => {
495+
next(error);
496+
},
497+
);
498+
});
499+
});
445500
});

0 commit comments

Comments
 (0)