diff --git a/packages/csv-parse/lib/index.d.ts b/packages/csv-parse/lib/index.d.ts index f5100150..f907df10 100644 --- a/packages/csv-parse/lib/index.d.ts +++ b/packages/csv-parse/lib/index.d.ts @@ -4,10 +4,10 @@ import * as stream from "stream"; -export type Callback = ( +export type Callback = ( err: CsvError | undefined, - records: any | undefined, - info: Info, + records: T[] | undefined, + info?: Info, ) => void; export interface Parser extends stream.Transform {} @@ -43,14 +43,19 @@ export type CastingDateFunction = ( context: CastingContext, ) => Date; -export type ColumnOption = string | undefined | null | false | { name: string }; +export type ColumnOption = + | 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 { /** * If true, the parser will attempt to convert read data types to native types. * @deprecated Use {@link cast} @@ -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. @@ -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. */ @@ -286,13 +300,28 @@ export class CsvError extends Error { ); } +type OptionsWithColumns = Omit, "columns"> & { + columns: Exclude; +}; + +declare function parse( + input: string | Buffer, + options: OptionsWithColumns, + callback?: Callback, +): 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( + options: OptionsWithColumns, + callback?: Callback, +): 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; diff --git a/packages/csv-parse/lib/sync.d.ts b/packages/csv-parse/lib/sync.d.ts index 97f70da0..dbaf9ba8 100644 --- a/packages/csv-parse/lib/sync.d.ts +++ b/packages/csv-parse/lib/sync.d.ts @@ -1,6 +1,16 @@ import { Options } from "./index.js"; -declare function parse(input: Buffer | string, options?: Options): any; +type OptionsWithColumns = Omit, "columns"> & { + columns: Exclude; +}; + +declare function parse( + input: Buffer | string, + options: OptionsWithColumns, +): T[]; +declare function parse(input: Buffer | string, options: Options): string[][]; +declare function parse(input: Buffer | string): string[][]; + // export default parse; export { parse }; diff --git a/packages/csv-parse/test/api.types.sync.ts b/packages/csv-parse/test/api.types.sync.ts index 11bc912f..8bd36a1c 100644 --- a/packages/csv-parse/test/api.types.sync.ts +++ b/packages/csv-parse/test/api.types.sync.ts @@ -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(""); @@ -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) { @@ -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("", { + columns: true, + }); + }); + }); }); diff --git a/packages/csv-parse/test/api.types.ts b/packages/csv-parse/test/api.types.ts index ab73ac45..25998c69 100644 --- a/packages/csv-parse/test/api.types.ts +++ b/packages/csv-parse/test/api.types.ts @@ -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 @@ -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); + }); }); }); @@ -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 = {}; + typedOptions.columns = ["age", undefined, null, false, { name: "name" }]; + typedOptions.columns = (record) => { + return ["age"]; + }; + + const unknownTypedOptions: Options = {}; + unknownTypedOptions.columns = ["anything", undefined, null, false]; + unknownTypedOptions.columns = (record) => { + return ["anything", undefined, null, false]; + }; }); it("group_columns_by_name", function () { @@ -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 () { @@ -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( + "", + { + columns: true, + }, + (error, records: Person[] | undefined) => { + next(error); + }, + ); + }); + }); });