@absoluteui/helpers/csv
RFC 4180 CSV parsing (including streaming) and generation with formula-injection protection.
import { ... } from '@absoluteui/helpers/csv';How to use
Section titled “How to use”RFC 4180 CSV in both directions: export tables that open safely in spreadsheets, and parse uploads, including large files as a stream. Reach for it for employee imports, payroll exports, and any “Download as CSV” button. To save the file, use downloadCsv from the services download module; the Files and downloads guide shows reading uploads in chunks.
How do I export a table safely?
Section titled “How do I export a table safely?”stringifyCsv quotes cells that need it and, by default, neutralises text a spreadsheet would run as a formula (=, +, -, @): a classic injection when user-entered names end up in an export. Numbers, booleans, and dates are never prefixed, so -250 stays numeric. Use columns to choose, order, rename, and format fields.
import { stringifyCsv } from '@absoluteui/helpers/csv';
const rows = [ { id: 'E-1001', name: 'Asha Rao', adjustment: -250, joined: new Date('2021-04-01T00:00:00Z') }, { id: 'E-1002', name: '=HYPERLINK("http://evil.test","Click")', adjustment: 0, joined: new Date('2024-01-15T00:00:00Z') },];const csv = stringifyCsv(rows, { columns: [ { key: 'id', header: 'Employee ID' }, 'name', { key: 'adjustment', header: 'Adjustment' }, { key: 'joined', header: 'Joined', format: (v) => (v instanceof Date ? v.toISOString().slice(0, 10) : v) }, ], eol: '\n',});csv.split('\n')[0]; // => 'Employee ID,name,Adjustment,Joined'csv.split('\n')[1]; // => 'E-1001,Asha Rao,-250,2021-04-01'csv.split('\n')[2]; // => `E-1002,"'=HYPERLINK(""http://evil.test"",""Click"")",0,2024-01-15`Add bom: true when the file is opened by Excel on Windows, so non-ASCII names display correctly.
How do I parse an uploaded file with a header row?
Section titled “How do I parse an uploaded file with a header row?”With header: true, rows are objects keyed by column name. Problems are collected in errors (with row and line numbers) instead of thrown, so you can show them next to the import. The delimiter is detected by default (,, ;, tab, or |), which handles files saved with ; by European spreadsheet settings.
import { parseCsv } from '@absoluteui/helpers/csv';
const text = 'Employee ID;Name;Dept\nE-1001;"Rao; Asha";Finance\nE-1002;Liam Chen\n';const result = parseCsv(text, { header: true, skipEmptyLines: true, trim: true });result.delimiter; // => ';'result.rows[0]; // => { 'Employee ID': 'E-1001', Name: 'Rao; Asha', Dept: 'Finance' }result.errors.map((e) => `${e.code} at row ${String(e.row)}`); // => ['HLP_CSV_FIELD_COUNT at row 3']How do I parse a large file without loading it all?
Section titled “How do I parse a large file without loading it all?”Feed text chunks to createCsvParser; each push returns the rows completed so far, and quotes or line breaks split across chunks are handled. Set delimiter when you know it: detection buffers the first 64 KiB (or 10 lines) before rows come out. Here a Blob stands in for the uploaded File.
import { createCsvParser } from '@absoluteui/helpers/csv';
const upload = new Blob(['id,name,salary\nE-1,Asha Rao,', '98000\nE-2,"Chen, ', 'Liam",91000\n']);const parser = createCsvParser({ header: true, delimiter: ',', maxRows: 100_000 });const reader = upload.stream().pipeThrough(new TextDecoderStream()).getReader();let total = 0;const names: string[] = [];for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) { for (const row of parser.push(chunk.value)) { total += Number(row.salary); names.push(row.name ?? ''); }}parser.end().length; // => 0names; // => ['Asha Rao', 'Chen, Liam']total; // => 189000How do I write one cell or check user input for formulas?
Section titled “How do I write one cell or check user input for formulas?”escapeCsvCell applies the same quoting and injection protection to a single value, for hand-built rows. isFormulaLike lets you warn at entry time instead of only at export.
import { escapeCsvCell, isFormulaLike } from '@absoluteui/helpers/csv';
escapeCsvCell('Rao, Asha'); // => '"Rao, Asha"'escapeCsvCell('@SUM(A1:A9)'); // => "'@SUM(A1:A9)"isFormulaLike('+44 20 7946 0000'); // => true // a phone number with + also triggers itisFormulaLike('Asha'); // => falseWhat goes wrong?
Section titled “What goes wrong?”Malformed rows go to errors; hard limits throw a LimitError so a hostile file cannot exhaust memory. Set maxRows, maxColumns, and maxFieldLength for uploads. Header names such as __proto__ are renamed to column_N and reported as HLP_CSV_UNSAFE_HEADER.
import { parseCsv } from '@absoluteui/helpers/csv';
parseCsv('a\n1\n2\n3', { maxRows: 2 }); // => throws HLP_CSV_TOO_MANY_ROWSparseCsv('note\n' + 'x'.repeat(100), { maxFieldLength: 50 }); // => throws HLP_CSV_FIELD_TOO_LONGparseCsv('name\n"unfinished', { header: true }).errors[0]?.code; // => 'HLP_CSV_UNCLOSED_QUOTE'parseCsv('__proto__,name\n1,Asha', { header: true }).headers; // => ['column_1', 'name']API reference
Section titled “API reference”Functions
Section titled “Functions”createCsvParser
Incremental CSV parser for large files: feed string chunks (for example from
TextDecoderStream) and collect rows as they complete. Quotes and CRLF split across chunks
are handled. With delimiter: 'auto', the first 64 KiB (or 10 lines) are buffered for detection.
function createCsvParser(options: CsvParseOptions & { readonly header: true;}): CsvStreamParser<Record<string, string>>;function createCsvParser(options?: CsvParseOptions & { readonly header?: false;}): CsvStreamParser<string[]>;function createCsvParser(options?: CsvParseOptions): CsvStreamParser<Record<string, string> | string[]>;Example:
const p = createCsvParser({ header: true, delimiter: ',' });p.push('name,dept\nAda,Eng'); // => []p.push('\nGrace,Ops\n'); // => [{ name: 'Ada', dept: 'Eng' }, { name: 'Grace', dept: 'Ops' }]p.end(); // => []detectDelimiter
Detects the delimiter from a sample by counting candidates outside quotes on the first
lines and preferring the most consistent, then most frequent. Defaults to ,.
function detectDelimiter(sample: string, candidates?: readonly string[], quote?: string): string;Example:
detectDelimiter('a;b;c\n1;2;3') // => ';'parseCsv
Parses CSV text (RFC 4180: quoted fields with delimiters and line breaks, "" escapes,
CRLF/LF/CR, BOM). With header, rows are objects keyed by column name; unsafe names such as
__proto__ are renamed (column_N) and reported. Malformed input is reported in errors,
never thrown; limits throw LimitError.
function parseCsv(text: string, options: CsvParseOptions & { readonly header: true;}): CsvResult<Record<string, string>>;function parseCsv(text: string, options?: CsvParseOptions & { readonly header?: false;}): CsvResult<string[]>;function parseCsv(text: string, options?: CsvParseOptions): CsvResult<Record<string, string> | string[]>;Example:
parseCsv('name,age\r\n"Doe, Jane",42', { header: true }).rows // => [{ name: 'Doe, Jane', age: '42' }]Interfaces
Section titled “Interfaces”CsvError
A problem found while parsing. row is the 1-based record number (the header is row 1).
interface CsvError { readonly row: number; readonly line: number; readonly code: 'HLP_CSV_UNCLOSED_QUOTE' | 'HLP_CSV_UNEXPECTED_QUOTE' | 'HLP_CSV_FIELD_COUNT' | 'HLP_CSV_UNSAFE_HEADER' | 'HLP_CSV_DUPLICATE_HEADER';}CsvParseOptions
interface CsvParseOptions { readonly delimiter?: string; readonly quote?: string; readonly header?: boolean; readonly trim?: boolean; readonly skipEmptyLines?: boolean; readonly comment?: string; readonly maxRows?: number; readonly maxFieldLength?: number; readonly maxColumns?: number;}CsvResult
interface CsvResult<R> { readonly rows: R[]; readonly headers: string[]; readonly errors: CsvError[]; readonly delimiter: string;}CsvStreamParser
Streaming parser handle returned by {@link createCsvParser}.
interface CsvStreamParser<R> { push(chunk: string): R[]; end(): R[]; readonly headers: readonly string[]; readonly errors: readonly CsvError[]; readonly delimiter: string | undefined;}Stringify
Section titled “Stringify”Functions
Section titled “Functions”escapeCsvCell
Escapes one cell: quotes it when it contains the delimiter, a quote, CR/LF, or leading or trailing spaces, doubling inner quotes; neutralizes formula-like text by default.
function escapeCsvCell(value: string, options?: EscapeCsvCellOptions): string;Example:
escapeCsvCell('say "hi"') // => '"say ""hi"""'Example:
escapeCsvCell('=SUM(A1)') // => "'=SUM(A1)"isFormulaLike
True when a spreadsheet could interpret the cell as a formula.
function isFormulaLike(cell: string): boolean;Example:
isFormulaLike('=1+1') // => truestringifyCsv
Writes rows (arrays or objects) as CSV. Numbers, booleans and dates are never prefixed by injection protection (a negative number stays numeric); strings are.
function stringifyCsv<T extends object>(rows: readonly T[] | readonly (readonly unknown[])[], options?: CsvStringifyOptions<T>): string;Example:
stringifyCsv([{ name: 'Ada', score: 1 }]) // => 'name,score\r\nAda,1\r\n'Example:
stringifyCsv([['a', 'b'], ['1', '2']], { eol: '\n' }) // => 'a,b\n1,2\n'Interfaces
Section titled “Interfaces”CsvColumn
A column for object rows.
interface CsvColumn<T> { readonly key: string; readonly header?: string; readonly format?: (value: unknown, row: T) => unknown;}CsvStringifyOptions
interface CsvStringifyOptions<T> extends EscapeCsvCellOptions { readonly columns?: readonly (string | CsvColumn<T>)[]; readonly header?: boolean; readonly eol?: '\r\n' | '\n'; readonly bom?: boolean; readonly trailingEol?: boolean;}EscapeCsvCellOptions
interface EscapeCsvCellOptions { readonly delimiter?: string; readonly quote?: 'minimal' | 'all'; readonly injectionProtection?: boolean;}