Skip to content

Commit a4089f9

Browse files
committed
feat(csv-parse): add generic type argument
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 c40c0d2 commit a4089f9

File tree

4 files changed

+113
-46
lines changed

4 files changed

+113
-46
lines changed

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

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as stream from "stream";
66

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

99
export interface Parser extends stream.Transform {}
1010

@@ -36,14 +36,14 @@ export type CastingFunction = (value: string, context: CastingContext) => any;
3636

3737
export type CastingDateFunction = (value: string, context: CastingContext) => Date;
3838

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

4141
/*
4242
Note, could not `extends stream.TransformOptions` because encoding can be
4343
BufferEncoding and undefined as well as null which is not defined in the
4444
extended type.
4545
*/
46-
export interface Options {
46+
export interface Options<T = string[]> {
4747
/**
4848
* If true, the parser will attempt to convert read data types to native types.
4949
* @deprecated Use {@link cast}
@@ -77,7 +77,10 @@ export interface Options {
7777
* default to null,
7878
* affect the result data set in the sense that records will be objects instead of arrays.
7979
*/
80-
columns?: ColumnOption[] | boolean | ((record: any) => ColumnOption[]);
80+
columns?:
81+
boolean |
82+
ColumnOption<T extends string[] ? string : keyof T>[] |
83+
((record: T) => ColumnOption<T extends string[] ? string : keyof T>[]);
8184
/**
8285
* Convert values into an array of values when columns are activated and
8386
* when multiple columns of the same name are found.
@@ -142,8 +145,8 @@ export interface Options {
142145
/**
143146
* Alter and filter records by executing a user defined function.
144147
*/
145-
on_record?: (record: any, context: CastingContext) => any;
146-
onRecord?: (record: any, context: CastingContext) => any;
148+
on_record?: (record: T, context: CastingContext) => T | null | undefined;
149+
onRecord?: (record: T, context: CastingContext) => T | null | undefined;
147150
/**
148151
* Optional character surrounding a field, one character only, defaults to double quotes.
149152
*/
@@ -274,9 +277,17 @@ export class CsvError extends Error {
274277
constructor(code: CsvErrorCode, message: string | string[], options?: Options, ...contexts: any[]);
275278
}
276279

277-
declare function parse(input: Buffer | string, options?: Options, callback?: Callback): Parser;
278-
declare function parse(input: Buffer | string, callback?: Callback): Parser;
279-
declare function parse(options?: Options, callback?: Callback): Parser;
280+
type OptionsWithColumns<T> = Omit<Options<T>, 'columns'> & {
281+
columns: Exclude<Options['columns'], undefined | false>
282+
}
283+
284+
declare function parse<T = unknown>(input: string | Buffer, options: OptionsWithColumns<T>, callback?: Callback<T>): Parser;
285+
declare function parse(input: string | Buffer, options: Options, callback?: Callback): Parser;
286+
287+
declare function parse<T = unknown>(options: OptionsWithColumns<T>, callback?: Callback<T>): Parser;
288+
declare function parse(options: Options, callback?: Callback): Parser;
289+
290+
declare function parse(input: string | Buffer, callback?: Callback): Parser;
280291
declare function parse(callback?: Callback): Parser;
281292

282293
// export default parse;

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11

22
import { Options } from './index.js';
33

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

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ import {
66
ColumnOption, Options, Info, CsvErrorCode, CsvError
77
} from '../lib/sync.js'
88

9-
describe('API Types', () => {
9+
describe('API Types - Sync', () => {
10+
type Person = {name: string, age: number}
1011

1112
it('respect parse signature', () => {
1213
// No argument
1314
parse("")
1415
parse("", {})
1516
parse(Buffer.from(""))
1617
parse(Buffer.from(""), {})
18+
parse(Buffer.from(""), {columns: true})
1719
})
1820

1921
it('return records', () => {
2022
try {
21-
const records: object = parse("")
23+
const records = parse("")
2224
typeof records
2325
}catch (err){
2426
if (err instanceof CsvError){
@@ -89,5 +91,29 @@ describe('API Types', () => {
8991
};
9092
return info;
9193
})
92-
94+
95+
describe('Generic types', () => {
96+
it('Exposes string[][] if columns is not specified', () => {
97+
const data: string[][] = parse("", {})
98+
})
99+
100+
it('Exposes string[][] if columns is falsy', () => {
101+
const data: string[][] = parse("", {
102+
columns: false
103+
})
104+
})
105+
106+
it('Exposes unknown[] if columns is specified as boolean', () => {
107+
const data: unknown[] = parse("", {
108+
columns: true
109+
})
110+
})
111+
112+
it('Exposes T[] if columns is specified', () => {
113+
const data: Person[] = parse<Person>("", {
114+
columns: true
115+
})
116+
})
117+
})
118+
93119
})

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

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

22
import 'should'
33
import { parse, CastingContext, Info, Options, Parser, CsvError } from '../lib/index.js'
4-
import { parse as parse_sync } from '../lib/sync.js'
54

65
describe('API Types', () => {
6+
type Person = {name: string, age: number}
77

88
describe('stream/callback API', () => {
99

@@ -52,40 +52,16 @@ describe('API Types', () => {
5252
})
5353

5454
it('Receive Callback', (next) => {
55-
parse('a\nb', function(err: Error|undefined, records: object, info: Info){
55+
parse('a\nb', function(err, records, info){
5656
if(err !== undefined){
57-
records.should.eql([['a'], ['b']])
58-
info.records.should.eql(2)
57+
records!.should.eql([['a'], ['b']])
58+
info!.records.should.eql(2)
5959
}
6060
next(err)
6161
})
6262
})
63-
64-
})
65-
66-
describe('sync api', () => {
67-
68-
it('respect parse signature', () => {
69-
// No argument
70-
parse_sync("")
71-
parse_sync("", {})
72-
parse_sync(Buffer.from(""))
73-
parse_sync(Buffer.from(""), {})
74-
})
75-
76-
it('return records', () => {
77-
try {
78-
const records: object = parse_sync("")
79-
typeof records
80-
}catch (err){
81-
if (err instanceof CsvError){
82-
err.message
83-
}
84-
}
85-
})
86-
8763
})
88-
64+
8965
describe('Info', () => {
9066

9167
const fakeinfo = {
@@ -175,16 +151,22 @@ describe('API Types', () => {
175151
options.columns = true
176152
options.columns = []
177153
options.columns = ['string', undefined, null, false, {name: 'column-name'}]
178-
options.columns = (record: string[]) => {
154+
options.columns = (record) => {
179155
const fields: string[] = record.map( (field: string) => {
180156
return field.toUpperCase()
181157
})
182158
return fields
183159
}
184-
options.columns = (record: string[]) => {
160+
options.columns = (record) => {
185161
record
186162
return ['string', undefined, null, false, {name: 'column-name'}]
187163
}
164+
165+
const typedOptions: Options<Person> = {}
166+
typedOptions.columns = ['age', undefined, null, false, {name: 'name'}]
167+
typedOptions.columns = (record) => {
168+
return ['age']
169+
}
188170
})
189171

190172
it('group_columns_by_name', () => {
@@ -252,9 +234,17 @@ describe('API Types', () => {
252234
it('on_record', () => {
253235
const options: Options = {}
254236
options.on_record = (record, {lines}) =>
255-
[lines, record[0]]
237+
[lines.toString(), record[0]]
256238
options.onRecord = (record, {lines}) =>
257-
[lines, record[0]]
239+
[lines.toString(), record[0]]
240+
241+
const typedOptions: Options<Person> = {}
242+
typedOptions.on_record = (record) => {
243+
return {...record, name: 'John Doe'}
244+
}
245+
typedOptions.onRecord = (record) => {
246+
return {...record, name: 'John Doe'}
247+
}
258248
})
259249

260250
it('quote', () => {
@@ -395,4 +385,37 @@ describe('API Types', () => {
395385
})
396386
})
397387
})
388+
389+
describe('Generic types', () => {
390+
it('Exposes string[][] if columns is not specified', (next) => {
391+
parse("", {}, (error, records: string[][] | undefined) => {
392+
next(error)
393+
})
394+
})
395+
396+
it('Exposes string[][] if columns is falsy', (next) => {
397+
parse("", {
398+
columns: false
399+
}, (error, records: string[][] | undefined) => {
400+
next(error)
401+
})
402+
})
403+
404+
it('Exposes unknown[] if columns is specified as boolean', (next) => {
405+
parse("", {
406+
columns: true
407+
}, (error, records: unknown[] | undefined) => {
408+
next(error)
409+
})
410+
})
411+
412+
it('Exposes T[] if columns is specified', (next) => {
413+
parse<Person>("", {
414+
columns: true
415+
}, (error, records: Person[] | undefined) => {
416+
next(error)
417+
})
418+
})
419+
})
420+
398421
})

0 commit comments

Comments
 (0)