API · Formats

csv()

Parse strings or byte chunks incrementally. Emit each CSV record as a string array or, when headers are enabled, as an object.

Signature

csv<H extends readonly PropertyKey[] | boolean = false>(
  options?: CsvOptions<H> | null,
): Exstream<CsvRow<H>, C>

interface CsvOptions<H extends readonly PropertyKey[] | boolean = false> {
  encoding?: string
  separator?: string
  quote?: string
  escape?: string
  fastMode?: boolean
  skipEmptyLines?: boolean
  header?: H | ((row: string[]) => readonly PropertyKey[])
  maxColumns?: number
  maxRecordBytes?: number
}

type CsvRow<H> = H extends readonly (infer K extends PropertyKey)[]
  ? Record<K, string>
  : H extends true
    ? Record<string, string>
    : string[]

options is optional. Passing null or undefined applies every default below. Other non-object values and arrays are rejected when the operator is created.

Example

const rows = exstream(csvChunks).csv({
  header: true,
  maxColumns: 100,
  maxRecordBytes: 8 * 1024 * 1024,
})

// { id: '1', total: '42.50' }

The parser does not convert cell values. Numbers, booleans, dates, empty fields, and whitespace all remain strings for an explicit downstream transformation.

Parameters

encoding

Type stringDefault 'utf8'

Accepts any non-empty encoding label supported by the active runtime. UTF-8 is portable across Node.js and browsers. Node.js uses its StringDecoder encodings; browser builds use WHATWG TextDecoder labels. For a non-UTF-8 browser source, provide byte chunks rather than JavaScript strings.

separator

Type stringDefault ','

Accepts any non-empty string that contains neither CR nor LF. Separators may contain several characters or Unicode code points, such as '||', '§', or '💥', and may cross input chunk boundaries.

quote

Type stringDefault '"'

Accepts exactly one Unicode character, including an astral character such as '💥'. A quoted field may contain separators, CR, LF, and escaped quote characters. Quotes may be split across chunks.

escape

Type stringDefault '"'

Accepts exactly one Unicode character. With the defaults, "" represents a literal quote inside a quoted field. When escape and quote differ, the escape character prefixes a quote and is doubled to represent itself. The parser does not require separator, quote, and escape to be distinct; choose a coherent dialect.

fastMode

Type booleanDefault false

Accepts only true or false. When enabled, quote recognition is disabled and quote characters are treated as ordinary data. Use it only when the input is guaranteed to have no quoted fields, embedded separators, or embedded line breaks.

skipEmptyLines

Type booleanDefault true

When true, a physically empty record is omitted. A quoted empty field, "", is still emitted as ['']. Set it to false when an empty physical line is meaningful; that line is then emitted as [''].

header

Type false | true | readonly PropertyKey[] | functionDefault false

Controls both header discovery and output shape. false emits every record as string[]. true consumes the first non-skipped record as keys. A non-empty array supplies keys without consuming an input record. A function receives the first non-skipped row, must return an array of keys, and consumes that row as the header. See Header modes for edge cases and typing.

maxColumns

Type positive integer | InfinityDefault Infinity

Limits the number of fields in each record, including empty fields. The parser accepts positive integers and Infinity; zero, negative numbers, fractions, and non-numeric values are rejected. Crossing the limit aborts the parser with code EXSTREAM_CSV_MAX_COLUMNS.

maxRecordBytes

Type positive integer | InfinityDefault Infinity

Limits one encoded CSV record, excluding its record delimiter. Counting uses encoding, so a character may occupy several bytes. The same numeric validation as maxColumns applies. Crossing the limit aborts the parser with code EXSTREAM_CSV_MAX_RECORD_BYTES.

All numeric limits are normalized with Number() at runtime, so any value coercing to a positive integer is accepted. TypeScript intentionally exposes them as numbers; use numeric values rather than relying on coercion.

Header modes

No header

The default emits every row, including the first, as an array:

exstream(['id,name\n1,Ada\n']).csv().valuesSync()
// [['id', 'name'], ['1', 'Ada']]

Header row

header: true consumes the first non-skipped row and uses its cells as object keys:

exstream(['id,name\n1,Ada\n']).csv({ header: true }).valuesSync()
// [{ id: '1', name: 'Ada' }]

An empty header array has the same runtime behavior as true: the first input row supplies the keys.

Explicit header

A non-empty array defines the keys before parsing begins, so the first input row remains data:

exstream(['1,Ada\n'])
  .csv({ header: ['id', 'name'] })
  .valuesSync()
// [{ id: '1', name: 'Ada' }]

Using a readonly tuple preserves exact keys in TypeScript:

const header = ['id', 'name'] as const
const rows = exstream(chunks).csv({ header })
// Exstream<Record<'id' | 'name', string>, C>

Header function

The function is called once with the first non-skipped row. Its returned array becomes the keys and that input row is not emitted:

const rows = exstream(chunks).csv<readonly string[]>({
  header: (row) => row.map((cell) => cell.trim().toLowerCase()),
})

The explicit generic gives callback-based headers an object output type. The callback must return an actual array; a thrown error or any other return value is a structural parse failure.

Uneven records

csv() does not require every record to have the same width. With object output, rows with fewer cells simply omit later keys. Rows with more cells than headers are not rejected: unmatched cells are assigned to the JavaScript key "undefined", and repeated or duplicate keys overwrite earlier values. Use maxColumns to cap width, and validate a fixed schema downstream when equal column counts matter.

Input and dialect

The input may contain strings, Buffer values in Node.js, Uint8Array, ArrayBuffer, or other typed-array views. A chunk can end anywhere: inside an encoded character, separator, escaped quote, quoted field, or CRLF pair. Empty chunks are ignored.

Records may end with LF, CRLF, or CR, and the last record does not need a trailing delimiter. Spaces are never trimmed. An empty source emits no rows.

quote and escape are validated only as single Unicode characters, so CR or LF technically satisfy the option contract and redefine how those characters are tokenized. Prefer non-newline dialect characters unless interoperability with such a format is intentional.

With quote parsing enabled, a quote may only begin at the start of a field. After its closing quote, only a separator or record delimiter is valid. Quoted fields may span physical lines; those line breaks become part of the cell string.

Streaming and limits

csv() is synchronous and preserves record order. It follows downstream demand and emits complete records as soon as they are parsed; it does not collect the full input. It must retain the current incomplete field and record, so memory can still grow with one unusually large record.

The output type retains the stream context type. When source chunks carry materialized contexts, a completed row inherits the context active on the chunk that completes it; contexts from all contributing chunks are not aggregated.

Set finite maxRecordBytes and maxColumns at untrusted boundaries. They default to Infinity because Exstream cannot infer an application-safe limit. Cancelling or destroying the branch stops further source consumption through the normal Exstream lifecycle.

Errors

Invalid option shapes and values throw synchronously when csv() is attached. Unsupported runtime encodings may fail when decoding begins.

Malformed input is a structural format failure: the parser emits a CsvParseError and aborts its branch because later record boundaries can no longer be trusted. The error exposes:

  • code — one of EXSTREAM_CSV_PARSE, EXSTREAM_CSV_UNTERMINATED_QUOTE, EXSTREAM_CSV_MAX_COLUMNS, or EXSTREAM_CSV_MAX_RECORD_BYTES
  • line and column — one-based physical location
  • offset — zero-based offset in the decoded input
  • record — one-based logical CSV record

exstream.errorInfo(error) also reports { origin: 'format', stage: 'csv' }.

Record errors already present upstream pass through unchanged. If they are handled downstream, parsing can continue because they do not alter the CSV decoder state.

Forms

csv() is available on streams and reusable pipelines. The direct standalone form takes options before the stream; the curried form is useful with through():

stream.csv(options)
exstream.pipeline().csv(options)
exstream.csv(options, stream)
stream.through(exstream.csv(options))

Pass null in the direct standalone form to apply defaults.

csvStringify(), json(), fetch a large CSV