Understanding the ExcelJS Type Conversion Problem

When working with Excel files in Node.js using exceljs, developers often encounter issues where cell values are automatically converted into JavaScript data types. For example, decimal numbers like 243,11 stored in localized formats (such as Hungarian or German) or numeric floating-point values can end up transformed into imprecise floats like 243.10999999999999.

This happens because Excel stores numbers internally as IEEE 754 double-precision floating-point numbers unless the cell is explicitly typed as text. When exceljs parses the file, it extracts the underlying raw numeric value rather than the formatted display string, leading to precision artifacts in JavaScript.

How ExcelJS Represents Cell Values

In exceljs, a cell object contains several properties:

  • cell.value: The raw underlying value, which can be a number, string, Date object, or an object representing formulas or rich text.
  • cell.text: The formatted display string calculated based on the cell's format rules (numFmt).
  • cell.type: An enum indicating the data type (e.g., ValueType.Number, ValueType.String, ValueType.Formula).

Solution: Creating a Robust Value Extractor

Relying solely on cell.text or direct string casting can fail if the cell contains complex objects like formulas, rich text, or missing format masks. To reliably extract the string value as written, you should handle each exceljs cell type explicitly.

Here is a complete, production-ready solution that extracts exact string values from any cell:

const exceljs = require('exceljs');

function getCellStringValue(cell) {
    if (cell.value === null || cell.value === undefined) {
        return '';
    }

    // 1. If formatted display text exists and is valid, prefer cell.text
    if (cell.text !== undefined && cell.text !== null && cell.text !== '') {
        return String(cell.text);
    }

    const val = cell.value;

    // 2. Handle Rich Text objects
    if (typeof val === 'object' && Array.isArray(val.richText)) {
        return val.richText.map(part => part.text).join('');
    }

    // 3. Handle Formula objects
    if (typeof val === 'object' && val.result !== undefined) {
        return String(val.result);
    }

    // 4. Handle Hyperlinks
    if (typeof val === 'object' && val.hyperlink) {
        return String(val.text || val.hyperlink);
    }

    // 5. Fix IEEE 754 floating point imprecision for standard numbers
    if (typeof val === 'number') {
        // Fixes artifacts like 243.10999999999999 back to 243.11
        return String(Number(val.toPrecision(12)));
    }

    return String(val);
}

async function readExcelFile(excelFile) {
    const table = [];
    const workbook = new exceljs.Workbook();
    await workbook.xlsx.readFile(excelFile);

    const worksheet = workbook.getWorksheet(1);

    worksheet.eachRow({ includeEmpty: false }, (row) => {
        const rowValues = [];
        row.eachCell({ includeEmpty: true }, (cell) => {
            rowValues.push(getCellStringValue(cell));
        });
        table.push(rowValues);
    });

    return table;
}

Handling Localized Decimal Separators (Commas vs. Dots)

If your Excel files use localized formatting (such as a comma , for decimal points) and you need to preserve that exact character when converting numbers back to strings, you can format the float output using String.prototype.replace() or toLocaleString():

function formatLocalizedNumber(numValue, locale = 'hu-HU') {
    if (typeof numValue === 'number') {
        const cleanNum = Number(numValue.toPrecision(12));
        return cleanNum.toLocaleString(locale);
    }
    return String(numValue);
}

Key Takeaways

  • Always check cell.text first when reading Excel files in ExcelJS to capture the formatted display value.
  • Use toPrecision(12) to strip away floating-point inaccuracies introduced by binary floating-point representation.
  • Inspect cell.value for complex types like rich text, hyperlink objects, and formula results to ensure no data is missed or rendered as [object Object].