Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit dd8196d

Browse files
committedMay 12, 2025··
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 dd8196d

File tree

4 files changed

+119
-46
lines changed

4 files changed

+119
-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 : (T extends unknown ? string : keyof T)>[] |
83+
((record: T) => ColumnOption<T extends string[] ? string : (T extends unknown ? 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: 62 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-
6463
})
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-
87-
})
88-
64+
8965
describe('Info', () => {
9066

9167
const fakeinfo = {
@@ -175,16 +151,28 @@ 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+
}
170+
171+
const unknownTypedOptions: Options<unknown> = {}
172+
unknownTypedOptions.columns = ['anything', undefined, null, false]
173+
unknownTypedOptions.columns = (record) => {
174+
return ['anything', undefined, null, false]
175+
}
188176
})
189177

190178
it('group_columns_by_name', () => {
@@ -252,9 +240,17 @@ describe('API Types', () => {
252240
it('on_record', () => {
253241
const options: Options = {}
254242
options.on_record = (record, {lines}) =>
255-
[lines, record[0]]
243+
[lines.toString(), record[0]]
256244
options.onRecord = (record, {lines}) =>
257-
[lines, record[0]]
245+
[lines.toString(), record[0]]
246+
247+
const typedOptions: Options<Person> = {}
248+
typedOptions.on_record = (record) => {
249+
return {...record, name: 'John Doe'}
250+
}
251+
typedOptions.onRecord = (record) => {
252+
return {...record, name: 'John Doe'}
253+
}
258254
})
259255

260256
it('quote', () => {
@@ -395,4 +391,37 @@ describe('API Types', () => {
395391
})
396392
})
397393
})
394+
395+
describe('Generic types', () => {
396+
it('Exposes string[][] if columns is not specified', (next) => {
397+
parse("", {}, (error, records: string[][] | undefined) => {
398+
next(error)
399+
})
400+
})
401+
402+
it('Exposes string[][] if columns is falsy', (next) => {
403+
parse("", {
404+
columns: false
405+
}, (error, records: string[][] | undefined) => {
406+
next(error)
407+
})
408+
})
409+
410+
it('Exposes unknown[] if columns is specified as boolean', (next) => {
411+
parse("", {
412+
columns: true
413+
}, (error, records: unknown[] | undefined) => {
414+
next(error)
415+
})
416+
})
417+
418+
it('Exposes T[] if columns is specified', (next) => {
419+
parse<Person>("", {
420+
columns: true
421+
}, (error, records: Person[] | undefined) => {
422+
next(error)
423+
})
424+
})
425+
})
426+
398427
})

0 commit comments

Comments
 (0)
Please sign in to comment.