Skip to content

toCsv function ​

Builds CSV text (RFC 4180): fields are quoted when needed, lines end with CRLF. Prepend '\uFEFF' so that Excel reads UTF-8.

ts
import { toCsv } from 'utils';

Signature ​

ts
export function toCsv(rows: Iterable<readonly unknown[]>, separator = ',', shouldEscapeFormulas = true): string

Parameters ​

NameDescription
rowsThe rows, header first; null and undefined give empty fields, dates their ISO text, objects their JSON.
separatorThe field separator, such as ';' for a locale with a decimal comma. Defaults to ','.
shouldEscapeFormulasWhether to prefix a text starting with =, +, - or @ with ', so that a spreadsheet does not run it. Defaults to true.

Returns ​

The CSV text, without a final line break.

Example ​

ts
toCsv([['time', 'speed'], [new Date(0), 12.5]]); // 'time,speed\r\n1970-01-01T00:00:00.000Z,12.5'

Tests ​

Line coverage: 100 %.

  • ✅ joins fields and lines
  • ✅ writes field 0 as a,b
  • ✅ writes field 1 as say "hi"
  • ✅ writes field 2 as two lines
  • ✅ writes field 3 as padded
  • ✅ writes field 4 as null
  • ✅ writes field 5 as Symbol(s)
  • ✅ writes field 6 as { a: 1 }
  • ✅ writes field 7 as 10n
  • ✅ writes field 8 as undefined
  • ✅ writes field 9 as true
  • ✅ uses the separator
  • ✅ neutralizes formulas on demand
  • ✅ returns an empty string without row

Source ​

src/string/to-csv.ts (on GitHub)

Source code
ts
const NEEDS_QUOTES = /[\n\r"]|^\s|\s$/v;
const FORMULA_START = /^[\t\r+\-=@]/v;

function toText(value: unknown): string {
  if (typeof value === 'string') {
    return value;
  }
  if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
    return String(value);
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  return typeof value === 'object' && value !== null ? JSON.stringify(value) : '';
}

function formatField(value: unknown, separator: string, shouldEscapeFormulas: boolean): string {
  let text = toText(value);
  if (shouldEscapeFormulas && typeof value === 'string' && FORMULA_START.test(text)) {
    text = `'${text}`;
  }
  return text.includes(separator) || NEEDS_QUOTES.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}

/**
 * Builds CSV text (RFC 4180): fields are quoted when needed, lines end with CRLF. Prepend `'\uFEFF'` so that
 * Excel reads UTF-8.
 *
 * @param rows - The rows, header first; `null` and `undefined` give empty fields, dates their ISO text,
 * objects their JSON.
 * @param separator - The field separator, such as `';'` for a locale with a decimal comma. Defaults to `','`.
 * @param shouldEscapeFormulas - Whether to prefix a text starting with `=`, `+`, `-` or `@` with `'`, so that a
 * spreadsheet does not run it. Defaults to `true`.
 * @returns The CSV text, without a final line break.
 * @example
 * toCsv([['time', 'speed'], [new Date(0), 12.5]]); // 'time,speed\r\n1970-01-01T00:00:00.000Z,12.5'
 */
export function toCsv(rows: Iterable<readonly unknown[]>, separator = ',', shouldEscapeFormulas = true): string {
  const lines: string[] = Array.from(rows, (row) =>
    row.map((value) => formatField(value, separator, shouldEscapeFormulas)).join(separator),
  );
  return lines.join('\r\n');
}
Test file
ts
import { toCsv } from './to-csv';

describe(toCsv, () => {
  it('joins fields and lines', () => {
    expect(
      toCsv(
        [
          ['time', 'speed'],
          [new Date(Date.UTC(2026, 0, 15, 12)), 12.5],
        ],
        ',',
        false,
      ),
    ).toBe('time,speed\r\n2026-01-15T12:00:00.000Z,12.5');
  });

  it.for([
    ['a,b', '"a,b"'],
    ['say "hi"', '"say ""hi"""'],
    ['two\nlines', '"two\nlines"'],
    [' padded', '" padded"'],
    [null, ''],
    [Symbol('s'), ''],
    [{ a: 1 }, '"{""a"":1}"'],
    [10n, '10'],
    [undefined, ''],
    [true, 'true'],
  ] as const)('writes field %# as %s', ([value, expected]) => {
    expect(toCsv([[value]], ',', false)).toBe(expected);
  });

  it('uses the separator', () => {
    expect(toCsv([['1,5', 2]], ';', false)).toBe('1,5;2');
    expect(toCsv([['a;b']], ';', false)).toBe('"a;b"');
  });

  it('neutralizes formulas on demand', () => {
    expect(toCsv([['=SUM(A1)', '-3', -3, 'ok']], ',', true)).toBe("'=SUM(A1),'-3,-3,ok");
    expect(toCsv([['=SUM(A1)']], ',', false)).toBe('=SUM(A1)');
  });

  it('returns an empty string without row', () => {
    expect(toCsv([], ',', false)).toBe('');
  });
});

Keywords: to, csv, strings, function

Generated from the JSDoc of the sources.