Skip to content

@absoluteui/helpers/csv

RFC 4180 CSV parsing (including streaming) and generation with formula-injection protection.

import { ... } from '@absoluteui/helpers/csv';

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.

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; // => 0
names; // => ['Asha Rao', 'Chen, Liam']
total; // => 189000

How 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 it
isFormulaLike('Asha'); // => false

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_ROWS
parseCsv('note\n' + 'x'.repeat(100), { maxFieldLength: 50 }); // => throws HLP_CSV_FIELD_TOO_LONG
parseCsv('name\n"unfinished', { header: true }).errors[0]?.code; // => 'HLP_CSV_UNCLOSED_QUOTE'
parseCsv('__proto__,name\n1,Asha', { header: true }).headers; // => ['column_1', 'name']

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' }]

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;
}

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') // => true

stringifyCsv

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'

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;
}